initial
This commit is contained in:
commit
baa0056244
352 changed files with 47928 additions and 0 deletions
8
.config/tsaoptions.json
Normal file
8
.config/tsaoptions.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"instanceUrl": "https://msazure.visualstudio.com",
|
||||
"projectName": "One",
|
||||
"areaPath": "One\\MGMT\\Compute\\Powershell\\Powershell",
|
||||
"notificationAliases": [ "andschwa@microsoft.com", "slee@microsoft.com" ],
|
||||
"codebaseName": "PowerShell_PowerShellEditorServices_20240313",
|
||||
"tools": [ "CredScan", "PoliCheck", "BinSkim" ]
|
||||
}
|
||||
239
.editorconfig
Normal file
239
.editorconfig
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{cs}]
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
file_header_template = Copyright (c) Microsoft Corporation.\nLicensed under the MIT License.
|
||||
csharp_space_before_open_square_brackets = true
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_style_expression_bodied_constructors = when_on_single_line
|
||||
csharp_style_expression_bodied_methods = when_on_single_line
|
||||
csharp_style_expression_bodied_operators = when_on_single_line
|
||||
csharp_style_expression_bodied_properties = true
|
||||
csharp_style_expression_bodied_indexers = true
|
||||
csharp_style_expression_bodied_accessors = true
|
||||
csharp_style_expression_bodied_lambdas = when_on_single_line
|
||||
|
||||
# TODO: Fix silenced and suggested rules!
|
||||
|
||||
# CS0168: The variable 'var' is declared but never used
|
||||
dotnet_diagnostic.CS0168.severity = error
|
||||
# CS0169: The private field 'class member' is never used
|
||||
dotnet_diagnostic.CS0169.severity = error
|
||||
# CS0219: The variable 'variable' is assigned but its value is never used
|
||||
dotnet_diagnostic.CS0219.severity = error
|
||||
# CS0414: The private field 'field' is assigned but its value is never used
|
||||
dotnet_diagnostic.CS0414.severity = error
|
||||
# CS0618: A class member was marked with the Obsolete attribute
|
||||
dotnet_diagnostic.CS0618.severity = suggestion
|
||||
# CS0649: Uninitialized private or internal field declaration that is never assigned a value
|
||||
dotnet_diagnostic.CS0649.severity = error
|
||||
# CS1570: Parameter has no matching param tag in the XML comment
|
||||
dotnet_diagnostic.CS1570.severity = silent
|
||||
# CS1574: XML comment has cref attribute that could not be resolved.
|
||||
dotnet_diagnostic.CS1574.severity = silent
|
||||
# CS1591: Missing XML comment for publicly visible type or member
|
||||
dotnet_diagnostic.CS1591.severity = silent
|
||||
# CS1998: This async method lacks 'await' operators and will run synchronously
|
||||
dotnet_diagnostic.CS1998.severity = suggestion
|
||||
# CS4014: Consider applying the await operator to the result of the call
|
||||
dotnet_diagnostic.CS4014.severity = suggestion
|
||||
|
||||
# CA1067: Should override Equals because it implements IEquatable<T>
|
||||
dotnet_diagnostic.CA1067.severity = silent
|
||||
# CA1068: CancellationToken parameters must come last
|
||||
dotnet_diagnostic.CA1068.severity = error
|
||||
# CA1501: Avoid excessive inheritance
|
||||
dotnet_diagnostic.CA1501.severity = error
|
||||
# CA1502: Avoid excessive complexity
|
||||
dotnet_diagnostic.CA1502.severity = silent
|
||||
# CA1505: Avoid unmaintainable code
|
||||
dotnet_diagnostic.CA1505.severity = error
|
||||
# CA1506: Avoid excessive class coupling
|
||||
dotnet_diagnostic.CA1506.severity = silent
|
||||
# CA1507: Use nameof in place of string
|
||||
dotnet_diagnostic.CA1507.severity = error
|
||||
# CA1508: Avoid dead conditional code
|
||||
dotnet_diagnostic.CA1508.severity = error
|
||||
# CA1802: Use Literals Where Appropriate
|
||||
dotnet_diagnostic.CA1802.severity = error
|
||||
# CA1805: Do not initialize unnecessarily.
|
||||
dotnet_diagnostic.CA1805.severity = error
|
||||
# CA1820: Test for empty strings using string length
|
||||
dotnet_diagnostic.CA1820.severity = error
|
||||
# CA1822: Mark members as static
|
||||
dotnet_diagnostic.CA1822.severity = error
|
||||
# CA1823: Avoid unused private fields
|
||||
dotnet_diagnostic.CA1823.severity = error
|
||||
# CA2007: Do not directly await a Task
|
||||
dotnet_diagnostic.CA2007.severity = error
|
||||
# CA2016: Forward the CancellationToken parameter to methods that take one
|
||||
dotnet_diagnostic.CA2016.severity = error
|
||||
# CA2213: Disposable fields should be disposed
|
||||
dotnet_diagnostic.CA2213.severity = error
|
||||
# CA2254: The logging message template should not vary between calls to 'LoggerExtensions.*'
|
||||
dotnet_diagnostic.CA2254.severity = silent
|
||||
|
||||
# RCS1049: Simplify boolean comparison
|
||||
dotnet_diagnostic.RCS1049.severity = error
|
||||
# RCS1102: Make class static
|
||||
dotnet_diagnostic.RCS1102.severity = error
|
||||
# RCS1139: Add summary element to documentation comment
|
||||
dotnet_diagnostic.RCS1139.severity = silent
|
||||
# RCS1194: Implement exception constructors
|
||||
dotnet_diagnostic.RCS1194.severity = suggestion
|
||||
# RCS1210: Return completed task instead of returning null
|
||||
dotnet_diagnostic.RCS1210.severity = error
|
||||
# RCS1036: Remove unnecessary blank line
|
||||
dotnet_diagnostic.RCS1036.severity = error
|
||||
# RCS1075: Avoid empty catch clause that catches System.Exception
|
||||
dotnet_diagnostic.RCS1075.severity = error
|
||||
# RCS1170: Use read-only auto-implemented property
|
||||
dotnet_diagnostic.RCS1170.severity = error
|
||||
|
||||
# VSTHRD002: Avoid problematic synchronous waits
|
||||
dotnet_diagnostic.VSTHRD002.severity = error
|
||||
# VSTHRD003: Avoid awaiting foreign Tasks
|
||||
dotnet_diagnostic.VSTHRD003.severity = error
|
||||
# VSTHRD105: Avoid method overloads that assume TaskScheduler.Current
|
||||
dotnet_diagnostic.VSTHRD105.severity = error
|
||||
# VSTHRD100: Avoid async void methods
|
||||
dotnet_diagnostic.VSTHRD100.severity = error
|
||||
# VSTHRD103: Call async methods when in an async method
|
||||
dotnet_diagnostic.VSTHRD103.severity = error
|
||||
# VSTHRD110: Observe result of async calls
|
||||
dotnet_diagnostic.VSTHRD110.severity = error
|
||||
# VSTHRD114: Avoid returning a null Task
|
||||
dotnet_diagnostic.VSTHRD114.severity = error
|
||||
# VSTHRD200: Use "Async" suffix for awaitable methods
|
||||
dotnet_diagnostic.VSTHRD200.severity = silent
|
||||
|
||||
# IDE0001: Simplify name
|
||||
dotnet_diagnostic.IDE0001.severity = error
|
||||
# IDE0002: Simplify member access
|
||||
dotnet_diagnostic.IDE0001.severity = error
|
||||
# IDE0003: Remove this or Me qualification
|
||||
dotnet_diagnostic.IDE0003.severity = error
|
||||
# IDE0004: Remove unnecessary cast
|
||||
dotnet_diagnostic.IDE0004.severity = error
|
||||
# IDE0005: Remove unnecessary import
|
||||
dotnet_diagnostic.IDE0005.severity = error
|
||||
# IDE0008: Use explicit type instead of var
|
||||
dotnet_diagnostic.IDE0008.severity = error
|
||||
# IDE0011: Add braces
|
||||
dotnet_diagnostic.IDE0011.severity = error
|
||||
# IDE0016: Use throw expression
|
||||
dotnet_diagnostic.IDE0016.severity = error
|
||||
# IDE0017: Use object initializers
|
||||
dotnet_diagnostic.IDE0017.severity = error
|
||||
# IDE0018: Inline variable declaration
|
||||
dotnet_diagnostic.IDE0018.severity = error
|
||||
# IDE0019: Use pattern matching to avoid 'as' followed by a 'null' check
|
||||
dotnet_diagnostic.IDE0019.severity = error
|
||||
# IDE0020: Use pattern matching to avoid 'is' check followed by a cast (with variable)
|
||||
dotnet_diagnostic.IDE0020.severity = error
|
||||
# IDE0021: Use expression body for constructors
|
||||
dotnet_diagnostic.IDE0021.severity = error
|
||||
# IDE0022: Use expression body for methods
|
||||
dotnet_diagnostic.IDE0022.severity = error
|
||||
# IDE0023: Use expression body for conversion operators
|
||||
dotnet_diagnostic.IDE0023.severity = error
|
||||
# IDE0024: Use expression body for operators
|
||||
dotnet_diagnostic.IDE0024.severity = error
|
||||
# IDE0025: Use expression body for properties
|
||||
dotnet_diagnostic.IDE0025.severity = error
|
||||
# IDE0026: Use expression body for indexers
|
||||
dotnet_diagnostic.IDE0026.severity = error
|
||||
# IDE0027: Use expression body for accessors
|
||||
dotnet_diagnostic.IDE0027.severity = error
|
||||
# IDE0028: Use collection initializers
|
||||
dotnet_diagnostic.IDE0028.severity = suggestion
|
||||
# IDE0029: Use coalesce expression (non-nullable types)
|
||||
dotnet_diagnostic.IDE0029.severity = error
|
||||
# IDE0030: Use coalesce expression (nullable types)
|
||||
dotnet_diagnostic.IDE0030.severity = error
|
||||
# IDE0031: Use null propagation
|
||||
dotnet_diagnostic.IDE0031.severity = error
|
||||
# IDE0032: Use auto property
|
||||
dotnet_diagnostic.IDE0032.severity = error
|
||||
# IDE0033: Use explicitly provided tuple name
|
||||
dotnet_diagnostic.IDE0033.severity = error
|
||||
# IDE0034: Simplify 'default' expression
|
||||
dotnet_diagnostic.IDE0034.severity = error
|
||||
# IDE0035: Remove unreachable code
|
||||
dotnet_diagnostic.IDE0035.severity = error
|
||||
# IDE0036: Order modifiers
|
||||
dotnet_diagnostic.IDE0036.severity = error
|
||||
# IDE0037: Use inferred member name
|
||||
dotnet_diagnostic.IDE0037.severity = error
|
||||
# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable)
|
||||
dotnet_diagnostic.IDE0038.severity = error
|
||||
# IDE0040: Add accessibility modifiers
|
||||
dotnet_diagnostic.IDE0040.severity = error
|
||||
# IDE0041: Use is null check
|
||||
dotnet_diagnostic.IDE0041.severity = error
|
||||
# IDE0042: Deconstruct variable declaration
|
||||
dotnet_diagnostic.IDE0042.severity = error
|
||||
# IDE0044: Add readonly modifier
|
||||
dotnet_diagnostic.IDE0044.severity = error
|
||||
# IDE0045: Use conditional expression for assignment
|
||||
dotnet_diagnostic.IDE0045.severity = error
|
||||
# IDE0046: Use conditional expression for return
|
||||
dotnet_diagnostic.IDE0046.severity = silent
|
||||
# IDE0047: Remove unnecessary parentheses
|
||||
dotnet_diagnostic.IDE0047.severity = error
|
||||
# IDE0049: Use language keywords instead of framework type names for type references
|
||||
dotnet_diagnostic.IDE0049.severity = error
|
||||
# IDE0051: Remove unused private member
|
||||
dotnet_diagnostic.IDE0051.severity = error
|
||||
# IDE0052: Remove unread private member
|
||||
dotnet_diagnostic.IDE0052.severity = error
|
||||
# IDE0053: Use expression body for lambdas
|
||||
dotnet_diagnostic.IDE0053.severity = error
|
||||
# IDE0054: Use compound assignment
|
||||
dotnet_diagnostic.IDE0054.severity = error
|
||||
# IDE0059: Unnecessary assignment of a value
|
||||
dotnet_diagnostic.IDE0059.severity = error
|
||||
# IDE0063: Use simple 'using' statement
|
||||
dotnet_diagnostic.IDE0063.severity = error
|
||||
# IDE0066: Use switch expression
|
||||
dotnet_diagnostic.IDE0066.severity = error
|
||||
# IDE0071: Simplify interpolation
|
||||
dotnet_diagnostic.IDE0071.severity = error
|
||||
# IDE0073: Require file header
|
||||
dotnet_diagnostic.IDE0073.severity = error
|
||||
# IDE0075: Simplify conditional expression
|
||||
dotnet_diagnostic.IDE0075.severity = error
|
||||
# IDE0078: Use pattern matching
|
||||
dotnet_diagnostic.IDE0078.severity = error
|
||||
# IDE0082: Convert typeof to nameof
|
||||
dotnet_diagnostic.IDE0082.severity = error
|
||||
# IDE0083: Use pattern matching (not operator)
|
||||
dotnet_diagnostic.IDE0083.severity = error
|
||||
# IDE0090: Simplify new expression
|
||||
dotnet_diagnostic.IDE0090.severity = suggestion
|
||||
# IDE0100: Remove unnecessary equality operator
|
||||
dotnet_diagnostic.IDE0100.severity = error
|
||||
# IDE0110: Remove unnecessary discard
|
||||
dotnet_diagnostic.IDE0110.severity = error
|
||||
# IDE1005: Use conditional delegate call
|
||||
dotnet_diagnostic.IDE1005.severity = error
|
||||
|
||||
[*.{json}]
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{ps1,psm1,psd1}]
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{ps1xml,props,xml,yaml}]
|
||||
indent_size = 2
|
||||
9
.git-blame-ignore-revs
Normal file
9
.git-blame-ignore-revs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Enable this file in your git config: git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
# Enabled on GitHub automatically
|
||||
|
||||
# Close Over APIs, was mostly reformatting
|
||||
945d61634784db2e51f894c9606e785a099fd23d
|
||||
# Dotnet Format Style
|
||||
44387b36695607248cebb9467ad48061c19354cb
|
||||
# Formatting Fixup
|
||||
7233182585b63760992545c7407b17fb2965bc5c
|
||||
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
CHANGELOG.md merge=union
|
||||
* text=auto
|
||||
*.png binary
|
||||
14
.github/CODEOWNERS
vendored
Normal file
14
.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# https://help.github.com/articles/about-codeowners/
|
||||
|
||||
# Global pattern
|
||||
* @PowerShell/extension
|
||||
|
||||
# Developer files
|
||||
src/ @PowerShell/extension-dev
|
||||
test/ @PowerShell/extension-dev
|
||||
tools/ @PowerShell/extension-dev
|
||||
.editorconfig @PowerShell/extension-dev
|
||||
.gitattributes @PowerShell/extension-dev
|
||||
.gitignore @PowerShell/extension-dev
|
||||
PowerShellEditorServices.build.ps1 @PowerShell/extension-dev
|
||||
PowerShellEditorServices.sln @PowerShell/extension-dev
|
||||
88
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
88
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
name: 🐛 Bug report
|
||||
description: Open an issue about a bug that needs fixing.
|
||||
labels: ["Issue-Bug", "Needs: Triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label: I have written a descriptive issue title.
|
||||
required: true
|
||||
- label: I have searched all [_open and closed_ issues](https://github.com/PowerShell/PowerShellEditorServices/issues?q=is%3Aissue) to ensure it has not already been reported.
|
||||
- label: I have read the [troubleshooting](https://github.com/PowerShell/vscode-powershell/blob/main/docs/troubleshooting.md) guide.
|
||||
- label: I am sure this issue is with _PowerShell Editor Services itself_ and does not reproduce in a standalone [PowerShell](https://github.com/PowerShell/PowerShell/issues/new/choose) instance, and is not an issue with my editor.
|
||||
- label: I have verified that I am using the latest version of PowerShell Editor Services.
|
||||
- label: If this is a security issue, I have read the [security issue reporting guidance](https://github.com/PowerShell/vscode-powershell/blob/main/SECURITY.md).
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Explain the problem briefly below.
|
||||
placeholder: I am experiencing a problem with X. I think Y should be happening but Z is actually happening.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: powershell-version
|
||||
attributes:
|
||||
label: PowerShell Version
|
||||
description: Paste verbatim output from `$PSVersionTable` below. Please double-check that this is the PowerShell version that is running Editor Services.
|
||||
render: console
|
||||
placeholder: |
|
||||
PS> $PSVersionTable
|
||||
|
||||
Name Value
|
||||
---- -----
|
||||
PSVersion 7.4.0
|
||||
PSEdition Core
|
||||
GitCommitId 7.4.0
|
||||
OS Darwin 20.4.0 Darwin Kernel
|
||||
Platform Unix
|
||||
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
|
||||
PSRemotingProtocolVersion 2.3
|
||||
SerializationVersion 1.1.0.1
|
||||
WSManStackVersion 3.0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: vscode-version
|
||||
attributes:
|
||||
label: Editor Version
|
||||
description: Paste verbatim output from `emacs --version` or `vim --version` etc. below. **Please also describe how you setup the relevant LSP client, such as `eglot` or `vim-lsp` and share your configuration!**
|
||||
render: console
|
||||
placeholder: |
|
||||
PS> emacs --version
|
||||
GNU Emacs 29.1
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: extension-version
|
||||
attributes:
|
||||
label: PowerShell Editor Services Version
|
||||
description: If you have interactive access to the PowerShell host running Editor Services, paste verbatim output of `$psEditor.EditorServicesVersion` below.
|
||||
render: console
|
||||
placeholder: |
|
||||
PS> $psEditor.EditorServicesVersion
|
||||
|
||||
Major Minor Build Revision
|
||||
----- ----- ----- --------
|
||||
3 14 0 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: List of steps, sample code, failing test or link to a project that reproduces the behavior. Make sure you place a stack trace inside a code (```) block to avoid linking unrelated issues.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: visuals
|
||||
attributes:
|
||||
label: Visuals
|
||||
description: Please upload images or animations that can be used to reproduce issues in the area below. Try the [Steps Recorder](https://support.microsoft.com/en-us/windows/record-steps-to-reproduce-a-problem-46582a9b-620f-2e36-00c9-04e25d784e47) on Windows or [Screenshot](https://support.apple.com/en-us/HT208721) on macOS.
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: Please upload logs collected by following these [instructions](https://github.com/PowerShell/vscode-powershell/blob/main/docs/troubleshooting.md#logs) for VS Code (otherwise however your editor is setup) in the area below. Be careful to scrub sensitive information!
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🙋 Ask a question
|
||||
url: https://aka.ms/powershell-vscode-discord
|
||||
about: "Find us in #vscode on the PowerShell Community Discord"
|
||||
26
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
26
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: ✨ Feature request
|
||||
description: Open an issue about a potential new feature or improvement.
|
||||
labels: ["Issue-Enhancement", "Needs: Triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label: I have written a descriptive issue title.
|
||||
required: true
|
||||
- label: I have searched all [issues](https://github.com/PowerShell/PowerShellEditorServices/issues?q=is%3Aissue) to ensure it has not already been requested.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Explain the feature request below.
|
||||
placeholder: I would like to do X because it would be useful for Y and I cannot currently do it with Z.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: proposed-design
|
||||
attributes:
|
||||
label: Proposed Design
|
||||
description: Optionally explain any technical design below.
|
||||
placeholder: We could accomplish this by extending X to take Y and yield Z.
|
||||
16
.github/release.yml
vendored
Normal file
16
.github/release.yml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- Ignore
|
||||
authors:
|
||||
- dependabot
|
||||
categories:
|
||||
- title: Enhancements & Features ✨
|
||||
labels:
|
||||
- Issue-Enhancement
|
||||
- title: Squashed Bugs 🐛
|
||||
labels:
|
||||
- Issue-Bug
|
||||
- title: Other Changes 🙏
|
||||
labels:
|
||||
- "*"
|
||||
68
.github/workflows/ci-test.yml
vendored
Normal file
68
.github/workflows/ci-test.yml
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
name: CI Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: dotnet
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-latest, macos-latest, ubuntu-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
cache: true
|
||||
cache-dependency-path: '**/*.csproj'
|
||||
global-json-file: ./global.json
|
||||
|
||||
- name: Install PSResources
|
||||
shell: pwsh
|
||||
run: ./tools/installPSResources.ps1
|
||||
|
||||
- name: Download PowerShell install script
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: PowerShell/PowerShell
|
||||
path: pwsh
|
||||
sparse-checkout: tools/install-powershell.ps1
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Install preview
|
||||
shell: pwsh
|
||||
run: ./pwsh/tools/install-powershell.ps1 -Preview -Destination ./preview
|
||||
|
||||
- name: If debugging, start upterm for interactive pipeline troubleshooting
|
||||
if: ${{ runner.debug == 1 }}
|
||||
uses: lhotari/action-upterm@v1
|
||||
with:
|
||||
wait-timeout-minutes: 1
|
||||
|
||||
- name: Build and test
|
||||
shell: pwsh
|
||||
run: Invoke-Build -Configuration Release TestFull
|
||||
|
||||
- name: Upload build artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PowerShellEditorServices-module-${{ matrix.os }}
|
||||
path: module
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: PowerShellEditorServices-test-results-${{ matrix.os }}
|
||||
path: '**/*.trx'
|
||||
45
.github/workflows/close-stale-issues.yml
vendored
Normal file
45
.github/workflows/close-stale-issues.yml
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
name: Issue Housekeeping
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
jobs:
|
||||
stale-resolved-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
name: Label resolved issues as needing fix verification
|
||||
with:
|
||||
any-of-labels: "Resolution-Answered,Resolution-Duplicate,Resolution-External,Resolution-Fixed,Resolution-Inactive"
|
||||
stale-issue-label: "Needs: Fix Verification"
|
||||
days-before-stale: 0
|
||||
days-before-close: -1
|
||||
stale-issue-message: "This issue has been labeled as resolved, please verify the provided fix (or other reason)."
|
||||
labels-to-remove-when-stale: "Needs: Maintainer Attention,Needs: Triage"
|
||||
stale-fixed-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
name: Close issues needing fix verification after 1 week of inactivity
|
||||
with:
|
||||
stale-issue-label: "Needs: Fix Verification"
|
||||
days-before-stale: -1
|
||||
labels-to-add-when-unstale: "Needs: Maintainer Attention"
|
||||
close-issue-reason: completed
|
||||
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."
|
||||
stale-feedback-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
name: Close issues needing author feedback after 1 week of inactivity
|
||||
with:
|
||||
stale-issue-label: "Needs: Author Feedback"
|
||||
days-before-stale: -1
|
||||
labels-to-add-when-unstale: "Needs: Maintainer Attention"
|
||||
labels-to-remove-when-unstale: "Needs: Triage"
|
||||
close-issue-reason: completed
|
||||
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."
|
||||
50
.github/workflows/emacs-test.yml
vendored
Normal file
50
.github/workflows/emacs-test.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: Emacs End-to-End Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
types: [ checks_requested ]
|
||||
|
||||
jobs:
|
||||
emacs:
|
||||
name: ert
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
cache: true
|
||||
cache-dependency-path: '**/*.csproj'
|
||||
|
||||
- name: Install PSResources
|
||||
shell: pwsh
|
||||
run: tools/installPSResources.ps1
|
||||
|
||||
- name: Build
|
||||
shell: pwsh
|
||||
run: Invoke-Build Build
|
||||
|
||||
- name: Install Emacs
|
||||
uses: purcell/setup-emacs@master
|
||||
with:
|
||||
version: '28.2'
|
||||
|
||||
- name: Run ERT with full CLI
|
||||
run: |
|
||||
emacs -Q --batch -f package-refresh-contents --eval "(package-install 'eglot)"
|
||||
emacs -Q --batch -l test/emacs-test.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
- name: Run ERT with simple CLI
|
||||
run: |
|
||||
emacs -Q --batch -f package-refresh-contents --eval "(package-install 'eglot)"
|
||||
emacs -Q --batch -l test/emacs-simple-test.el -f ert-run-tests-batch-and-exit
|
||||
76
.github/workflows/vim-test.yml
vendored
Normal file
76
.github/workflows/vim-test.yml
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
name: Vim End-to-End Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
merge_group:
|
||||
types: [ checks_requested ]
|
||||
|
||||
jobs:
|
||||
vim:
|
||||
name: themis
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
cache: true
|
||||
cache-dependency-path: '**/*.csproj'
|
||||
|
||||
- name: Install PSResources
|
||||
shell: pwsh
|
||||
run: tools/installPSResources.ps1
|
||||
|
||||
- name: Build
|
||||
shell: pwsh
|
||||
run: Invoke-Build Build
|
||||
|
||||
- name: Install Vim
|
||||
id: vim
|
||||
uses: rhysd/action-setup-vim@v1
|
||||
with:
|
||||
version: nightly
|
||||
|
||||
- name: Checkout vim-ps1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: PProvost/vim-ps1
|
||||
path: vim-ps1
|
||||
|
||||
- name: Checkout LanguageClient-neovim
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: autozimu/LanguageClient-neovim
|
||||
path: LanguageClient-neovim
|
||||
|
||||
- name: Install LanguageClient-neovim
|
||||
run: ./install.sh
|
||||
working-directory: LanguageClient-neovim
|
||||
|
||||
- name: Checkout Themis
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: thinca/vim-themis
|
||||
path: vim-themis
|
||||
|
||||
# - name: Debug if run with debugging enabled
|
||||
# uses: lhotari/action-upterm@v1
|
||||
|
||||
- name: Run Themis with full CLI
|
||||
env:
|
||||
THEMIS_VIM: ${{ steps.vim.outputs.executable }}
|
||||
run: ./vim-themis/bin/themis ./test/vim-test.vim
|
||||
|
||||
- name: Run Themis with simple CLI
|
||||
env:
|
||||
THEMIS_VIM: ${{ steps.vim.outputs.executable }}
|
||||
run: ./vim-themis/bin/themis ./test/vim-simple-test.vim
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
bin/
|
||||
obj/
|
||||
module/
|
||||
TestResults/
|
||||
1
.mailmap
Normal file
1
.mailmap
Normal file
|
|
@ -0,0 +1 @@
|
|||
Andy Jordan <andy.jordan@microsoft.com> <andrew@schwartzmeyer.com>
|
||||
5
.markdownlint.json
Normal file
5
.markdownlint.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"MD013": false,
|
||||
"MD033": false,
|
||||
"MD022": false
|
||||
}
|
||||
174
.pipelines/PowerShellEditorServices-OneBranch.yml
Normal file
174
.pipelines/PowerShellEditorServices-OneBranch.yml
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
#################################################################################
|
||||
# OneBranch Pipelines #
|
||||
# This pipeline was created by EasyStart from a sample located at: #
|
||||
# https://aka.ms/obpipelines/easystart/samples #
|
||||
# Documentation: https://aka.ms/obpipelines #
|
||||
# Yaml Schema: https://aka.ms/obpipelines/yaml/schema #
|
||||
# Retail Tasks: https://aka.ms/obpipelines/tasks #
|
||||
# Support: https://aka.ms/onebranchsup #
|
||||
#################################################################################
|
||||
|
||||
trigger:
|
||||
- main
|
||||
|
||||
schedules:
|
||||
- cron: "35 13 * * 4"
|
||||
displayName: Weekly CodeQL
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
always: true
|
||||
|
||||
resources:
|
||||
repositories:
|
||||
- repository: templates
|
||||
type: git
|
||||
name: OneBranch.Pipelines/GovernedTemplates
|
||||
ref: refs/heads/main
|
||||
|
||||
parameters:
|
||||
- name: debug
|
||||
displayName: Enable debug output
|
||||
type: boolean
|
||||
default: false
|
||||
- name: OfficialBuild
|
||||
displayName: Use Official OneBranch template
|
||||
type: boolean
|
||||
default: true
|
||||
- name: Release
|
||||
displayName: Generate a release
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
variables:
|
||||
system.debug: ${{ parameters.debug }}
|
||||
BuildConfiguration: Release
|
||||
WindowsContainerImage: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
|
||||
OneBranchTemplate: ${{ iif(parameters.OfficialBuild, 'v2/OneBranch.Official.CrossPlat.yml@templates', 'v2/OneBranch.NonOfficial.CrossPlat.yml@templates') }}
|
||||
|
||||
extends:
|
||||
# https://aka.ms/obpipelines/templates
|
||||
template: ${{ variables.OneBranchTemplate }}
|
||||
parameters:
|
||||
globalSdl: # https://aka.ms/obpipelines/sdl
|
||||
asyncSdl:
|
||||
enabled: true
|
||||
forStages: [build]
|
||||
featureFlags:
|
||||
EnableCDPxPAT: false
|
||||
WindowsHostVersion:
|
||||
Version: 2022
|
||||
Network: KS3
|
||||
release:
|
||||
category: NonAzure
|
||||
stages:
|
||||
- stage: build
|
||||
jobs:
|
||||
- job: main
|
||||
displayName: Build package
|
||||
pool:
|
||||
type: windows
|
||||
variables:
|
||||
ob_outputDirectory: $(Build.SourcesDirectory)/out
|
||||
steps:
|
||||
- pwsh: |
|
||||
[xml]$xml = Get-Content PowerShellEditorServices.Common.props
|
||||
$version = $xml.Project.PropertyGroup.VersionPrefix
|
||||
$prerelease = $xml.Project.PropertyGroup.VersionSuffix
|
||||
if ($prerelease) { $version += "-$prerelease" }
|
||||
Write-Output "##vso[task.setvariable variable=version;isOutput=true]$version"
|
||||
Write-Output "##vso[task.setvariable variable=prerelease;isOutput=true]$(-not [string]::IsNullOrEmpty($prerelease))"
|
||||
name: package
|
||||
displayName: Get version from project properties
|
||||
- task: onebranch.pipeline.version@1
|
||||
displayName: Set OneBranch version
|
||||
inputs:
|
||||
system: Custom
|
||||
customVersion: $(package.version)
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET 8.x SDK
|
||||
inputs:
|
||||
packageType: sdk
|
||||
useGlobalJson: true
|
||||
- pwsh: ./tools/installPSResources.ps1 -PSRepository CFS
|
||||
displayName: Install PSResources
|
||||
- pwsh: Invoke-Build TestFull -Configuration $(BuildConfiguration) -PSRepository CFS
|
||||
displayName: Build and test
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish test results
|
||||
inputs:
|
||||
testRunner: VSTest
|
||||
testResultsFiles: "**/*.trx"
|
||||
failTaskOnFailedTests: true
|
||||
- pwsh: |
|
||||
$assembly = [Reflection.Assembly]::LoadFile("$(Build.SourcesDirectory)/module/PowerShellEditorServices/bin/Core/Microsoft.PowerShell.EditorServices.Hosting.dll")
|
||||
if ($assembly.GetCustomAttributes([System.Diagnostics.DebuggableAttribute], $true).IsJITOptimizerDisabled) {
|
||||
Write-Host "##vso[task.LogIssue type=error;]Was not built in release configuration!"
|
||||
exit 1
|
||||
}
|
||||
displayName: Assert release configuration
|
||||
continueOnError: true
|
||||
- task: onebranch.pipeline.signing@1
|
||||
displayName: Sign 1st-party files
|
||||
inputs:
|
||||
command: sign
|
||||
signing_profile: external_distribution
|
||||
search_root: $(Build.SourcesDirectory)/module
|
||||
files_to_sign: |
|
||||
**/*.ps1;
|
||||
**/*.psd1;
|
||||
**/*.psm1;
|
||||
**/*.ps1xml;
|
||||
**/Microsoft.PowerShell.EditorServices*.dll;
|
||||
- task: onebranch.pipeline.signing@1
|
||||
displayName: Sign 3rd-party files
|
||||
inputs:
|
||||
command: sign
|
||||
signing_profile: 135020002
|
||||
search_root: $(Build.SourcesDirectory)/module
|
||||
files_to_sign: |
|
||||
**/MediatR.dll;
|
||||
**/Nerdbank.Streams.dll;
|
||||
**/Newtonsoft.Json.dll;
|
||||
**/OmniSharp.Extensions*.dll;
|
||||
**/System.Reactive.dll;
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Zip signed artifacts
|
||||
inputs:
|
||||
rootFolderOrFile: $(Build.SourcesDirectory)/module
|
||||
includeRootFolder: false
|
||||
archiveType: zip
|
||||
archiveFile: out/PowerShellEditorServices.zip
|
||||
- stage: release
|
||||
dependsOn: build
|
||||
condition: and(succeeded(), ${{ eq(parameters.Release, true) }})
|
||||
variables:
|
||||
ob_release_environment: ${{ iif(parameters.OfficialBuild, 'Production', 'Test') }}
|
||||
version: $[ stageDependencies.build.main.outputs['package.version'] ]
|
||||
prerelease: $[ stageDependencies.build.main.outputs['package.prerelease'] ]
|
||||
jobs:
|
||||
- job: github
|
||||
displayName: Publish draft to GitHub
|
||||
pool:
|
||||
type: release
|
||||
templateContext:
|
||||
inputs:
|
||||
- input: pipelineArtifact
|
||||
artifactName: drop_build_main
|
||||
steps:
|
||||
- task: GitHubRelease@1
|
||||
displayName: Create GitHub release
|
||||
inputs:
|
||||
gitHubConnection: github.com_andyleejordan
|
||||
repositoryName: PowerShell/PowerShellEditorServices
|
||||
target: main
|
||||
assets: $(Pipeline.Workspace)/PowerShellEditorServices.zip
|
||||
tagSource: userSpecifiedTag
|
||||
tag: v$(version)
|
||||
isDraft: true
|
||||
isPreRelease: $(prerelease)
|
||||
addChangeLog: false
|
||||
releaseNotesSource: inline
|
||||
releaseNotesInline: "<!-- TODO: Generate release notes on GitHub! -->"
|
||||
2279
CHANGELOG.md
Normal file
2279
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
10
CODE_OF_CONDUCT.md
Normal file
10
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Microsoft Open Source Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
|
||||
Resources:
|
||||
|
||||
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
|
||||
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
|
||||
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
|
||||
- Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support)
|
||||
154
CONTRIBUTING.md
Normal file
154
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# Contribution Guidelines
|
||||
|
||||
We welcome many kinds of community contributions to this project! Whether it's a feature implementation,
|
||||
bug fix, or a good idea, please create an issue so that we can discuss it. It is not necessary to create an
|
||||
issue before sending a pull request but it may speed up the process if we can discuss your idea before
|
||||
you start implementing it.
|
||||
|
||||
Because this project exposes a couple different public APIs, we must be very mindful of any potential breaking
|
||||
changes. Some contributions may not be accepted if they risk introducing breaking changes or if they
|
||||
don't match the goals of the project. The core maintainer team has the right of final approval over
|
||||
any contribution to this project. However, we are very happy to hear community feedback on any decision
|
||||
so that we can ensure we are solving the right problems in the right way.
|
||||
|
||||
**NOTE**: If you believe there is a security vulnerability, please see [Security Reporting](#security-reporting).
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
- File a bug or feature request as an [issue](https://github.com/PowerShell/PowerShellEditorServices/issues)
|
||||
- Comment on existing issues to give your feedback on how they should be fixed/implemented
|
||||
- Contribute a bug fix or feature implementation by submitting a pull request
|
||||
- Contribute more unit tests for feature areas that lack good coverage
|
||||
- Review the pull requests that others submit to ensure they follow [established guidelines](#follow-the-pull-request-process)
|
||||
- Help others gets started with the project by contributing documentation or chatting
|
||||
in the #editors or #vscode rooms in the [PowerShell community Slack chat](http://slack.poshcode.org).
|
||||
|
||||
## Code Contribution Guidelines
|
||||
|
||||
Here's a high level list of guidelines to follow to ensure your code contribution is accepted:
|
||||
|
||||
- Follow established guidelines for coding style and design
|
||||
- Follow established guidelines for commit hygiene
|
||||
- Write unit tests to validate new features and bug fixes
|
||||
- Ensure that the 'Release' build and unit tests pass locally
|
||||
- Ensure that the Azure DevOps build passes for your change
|
||||
- Respond to all review feedback and final commit cleanup
|
||||
|
||||
### Practice Good Commit Hygiene
|
||||
|
||||
First of all, make sure you are practicing [good commit hygiene](http://blog.ericbmerritt.com/2011/09/21/commit-hygiene-and-git.html)
|
||||
so that your commits provide a good history of the changes you are making. To be more specific:
|
||||
|
||||
- **Write good commit messages**
|
||||
|
||||
Commit messages should be clearly written so that a person can look at the commit log and understand
|
||||
how and why a given change was made. Here is a great model commit message taken from a [blog post
|
||||
by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html):
|
||||
|
||||
Capitalized, short (50 chars or less) summary
|
||||
|
||||
More detailed explanatory text, if necessary. Wrap it to about 72
|
||||
characters or so. In some contexts, the first line is treated as the
|
||||
subject of an email and the rest of the text as the body. The blank
|
||||
line separating the summary from the body is critical (unless you omit
|
||||
the body entirely); tools like rebase can get confused if you run the
|
||||
two together.
|
||||
|
||||
Write your commit message in the imperative: "Fix bug" and not "Fixed bug"
|
||||
or "Fixes bug." This convention matches up with commit messages generated
|
||||
by commands like git merge and git revert.
|
||||
|
||||
Further paragraphs come after blank lines.
|
||||
|
||||
- Bullet points are okay, too
|
||||
|
||||
- Typically a hyphen or asterisk is used for the bullet, followed by a
|
||||
single space, with blank lines in between, but conventions vary here
|
||||
|
||||
- Use a hanging indent
|
||||
|
||||
A change that fixes a known bug with an issue filed should use the proper syntax so that the [issue
|
||||
is automatically closed](https://help.github.com/articles/closing-issues-via-commit-messages/) once
|
||||
your change is merged. Here's an example of what such a commit message should look like:
|
||||
|
||||
Fix #3: Catch NullReferenceException from DoThing
|
||||
|
||||
This change adds a try/catch block to catch a NullReferenceException that
|
||||
gets thrown by DoThing [...]
|
||||
|
||||
- **Squash your commits**
|
||||
|
||||
If you are introducing a new feature but have implemented it over multiple commits,
|
||||
please [squash those commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
|
||||
into a single commit that contains all the changes in one place. This especially applies to any "oops"
|
||||
commits where a file is forgotten or a typo is being fixed. Following this approach makes it a lot easier
|
||||
to pull those changes to other branches or roll back the change if necessary.
|
||||
|
||||
- **Keep individual commits for larger changes**
|
||||
|
||||
You can certainly maintain individual commits for different phases of a big change. For example, if
|
||||
you want to reorganize some files before adding new functionality, have your first commit contain all
|
||||
of the file move changes and then the following commit can have all of the feature additions. We
|
||||
highly recommend this approach so that larger commits don't turn into a jumbled mess.
|
||||
|
||||
### Add Unit Tests for New Code
|
||||
|
||||
If you're adding a new feature to the project, please make sure to include adequate [xUnit](http://xunit.github.io/)
|
||||
tests with your change. In this project, we have chosen write out unit tests in a way that uses the
|
||||
actual PowerShell environment rather than extensive interface mocking. This allows us to be sure that
|
||||
our features will work in practice.
|
||||
|
||||
We do both component-level and scenario-level testing depending on what code is being tested. We don't
|
||||
expect contributors to test every possible edge case. Testing mainline scenarios and the most common
|
||||
failure scenarios is often good enough.
|
||||
|
||||
We are very happy to accept unit test contributions for any feature areas that are more error-prone than
|
||||
others. Also, if you find that a feature fails for you in a specific case, please feel free to file an issue
|
||||
that includes a unit test which reproduces the problem. This will allow us to quickly implement a fix
|
||||
that resolves the problem.
|
||||
|
||||
### Build 'Release' Before Submitting
|
||||
|
||||
Before you send out your pull request, make sure that you have run a Release configuration build of the
|
||||
project and that all new and existing tests are passing. The Release configuration build ensures that
|
||||
all public API interfaces have been documented correctly otherwise it throws an error. We have turned
|
||||
on this check so that our project will always have good generated documentation.
|
||||
|
||||
### Follow the Pull Request Process
|
||||
|
||||
- **Create your pull request**
|
||||
|
||||
Use the [typical process](https://help.github.com/articles/using-pull-requests/) to send a pull request
|
||||
from your fork of the project. In your pull request message, please give a high-level summary of the
|
||||
changes that you have made so that reviewers understand the intent of the changes. You should receive
|
||||
initial comments within a day or two, but please feel free to ping if things are taking longer than
|
||||
expected.
|
||||
|
||||
- **The build and unit tests must run green**
|
||||
|
||||
When you submit your pull request, our automated build system on Azure DevOps will attempt to run a
|
||||
Release build of your changes and then run all unit tests against the build. If you notice that
|
||||
any of your unit tests have failed, please fix them by creating a new commit and then pushing it
|
||||
to your branch. If you see that some unrelated test has failed, try re-running the build for your
|
||||
pull request. If you continue to see issues, write a comment on the pull request and we will
|
||||
look into it.
|
||||
|
||||
- **Respond to code review feedback**
|
||||
|
||||
If the reviewers ask you to make changes, make them as a new commit to your branch and push them so
|
||||
that they are made available for a final review pass. Do not rebase the fixes just yet so that the
|
||||
commit hash changes don't upset GitHub's pull request UI.
|
||||
|
||||
- **If necessary, do a final rebase**
|
||||
|
||||
Once your final changes have been accepted, we may ask you to do a final rebase to have your commits
|
||||
so that they follow our commit guidelines. If specific guidance is given, please follow it when
|
||||
rebasing your commits. Once you do your final push and we see the Azure DevOps build pass, we will
|
||||
merge your changes!
|
||||
|
||||
### Security Reporting
|
||||
|
||||
If you believe that there is a security vulnerability in the PowerShell extension for VSCode,
|
||||
it **must** be reported to [secure@microsoft.com](https://technet.microsoft.com/security/ff852094.aspx) to allow for [Coordinated Vulnerability Disclosure](https://technet.microsoft.com/security/dn467923).
|
||||
**Only** file an issue, if secure@microsoft.com has confirmed filing an issue is appropriate.
|
||||
Please also CC in [vscode-powershell@microsoft.com](mailto:vscode-powershell@microsoft.com).
|
||||
5
Directory.Build.props
Normal file
5
Directory.Build.props
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
37
Directory.Packages.props
Normal file
37
Directory.Packages.props
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageVersion Include="Microsoft.PowerShell.5.ReferenceAssemblies" Version="1.1.0" />
|
||||
<PackageVersion Include="Microsoft.PowerShell.SDK" Version="7.4.13" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageVersion Include="OmniSharp.Extensions.DebugAdapter.Client" Version="0.19.9" />
|
||||
<PackageVersion Include="OmniSharp.Extensions.DebugAdapter.Server" Version="0.19.9" />
|
||||
<PackageVersion Include="OmniSharp.Extensions.LanguageClient" Version="0.19.9" />
|
||||
<PackageVersion Include="OmniSharp.Extensions.LanguageServer" Version="0.19.9" />
|
||||
<PackageVersion Include="PowerShellStandard.Library" Version="5.1.1" />
|
||||
<PackageVersion Include="Roslynator.Analyzers" Version="4.14.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageVersion>
|
||||
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageVersion>
|
||||
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageVersion>
|
||||
<PackageVersion Include="System.IO.Pipes.AccessControl" Version="5.0.0" />
|
||||
<PackageVersion Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Security.Principal" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
|
||||
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
3496
NOTICE.txt
Normal file
3496
NOTICE.txt
Normal file
File diff suppressed because it is too large
Load diff
21
PowerShellEditorServices.Common.props
Normal file
21
PowerShellEditorServices.Common.props
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>4.4.0</VersionPrefix>
|
||||
<VersionSuffix></VersionSuffix>
|
||||
<Company>Microsoft</Company>
|
||||
<Copyright>© Microsoft Corporation.</Copyright>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<PackageTags>PowerShell;editor;development;language;debugging</PackageTags>
|
||||
<PackageLicenseUrl>https://raw.githubusercontent.com/PowerShell/PowerShellEditorServices/main/LICENSE</PackageLicenseUrl>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/PowerShell/PowerShellEditorServices</RepositoryUrl>
|
||||
<DebugType>portable</DebugType>
|
||||
<!-- See: https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview -->
|
||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<!-- Required to enable IDE0005 as error -->
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- TODO: Enable <AnalysisMode>All</AnalysisMode> -->
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
406
PowerShellEditorServices.build.ps1
Normal file
406
PowerShellEditorServices.build.ps1
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
param(
|
||||
[ValidateSet("Debug", "Release")]
|
||||
[string]$Configuration = "Debug",
|
||||
|
||||
[switch]$LocalOmniSharp,
|
||||
|
||||
[string]$PSRepository = "PSGallery",
|
||||
|
||||
[string]$Verbosity = "minimal",
|
||||
|
||||
# See: https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests
|
||||
[string]$TestFilter = '',
|
||||
|
||||
# See: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test
|
||||
[string[]]$TestArgs = @("--logger", "console;verbosity=minimal", "--logger", "trx")
|
||||
)
|
||||
|
||||
#Requires -Modules @{ModuleName = "InvokeBuild"; ModuleVersion = "5.0.0"}
|
||||
#Requires -Modules @{ModuleName = "platyPS"; ModuleVersion = "0.14.2"}
|
||||
|
||||
$script:dotnetBuildArgs = @(
|
||||
"--verbosity"
|
||||
$Verbosity
|
||||
"--nologo"
|
||||
"-c"
|
||||
$Configuration
|
||||
if ($LocalOmniSharp) { "-property:LocalOmniSharp=true" }
|
||||
)
|
||||
|
||||
$script:dotnetTestArgs = @("test") + $script:dotnetBuildArgs + $TestArgs + @(
|
||||
if ($TestFilter) { "--filter", $TestFilter }
|
||||
"--framework"
|
||||
)
|
||||
|
||||
$script:IsNix = $IsLinux -or $IsMacOS
|
||||
$script:BuildInfoPath = "src/PowerShellEditorServices.Hosting/BuildInfo.cs"
|
||||
|
||||
$script:NetFramework = @{
|
||||
PS51 = 'net462'
|
||||
PS74 = 'net8.0'
|
||||
Standard = 'netstandard2.0'
|
||||
}
|
||||
|
||||
$script:HostCoreOutput = "src/PowerShellEditorServices.Hosting/bin/$Configuration/$($script:NetFramework.PS74)/publish"
|
||||
$script:HostDeskOutput = "src/PowerShellEditorServices.Hosting/bin/$Configuration/$($script:NetFramework.PS51)/publish"
|
||||
$script:PsesOutput = "src/PowerShellEditorServices/bin/$Configuration/$($script:NetFramework.Standard)/publish"
|
||||
|
||||
if (Get-Command git -ErrorAction SilentlyContinue) {
|
||||
# ignore changes to this file
|
||||
git update-index --assume-unchanged $script:BuildInfoPath
|
||||
}
|
||||
|
||||
Task FindDotNet {
|
||||
Assert (Get-Command dotnet -ErrorAction SilentlyContinue) "dotnet not found, please install it: https://aka.ms/dotnet-cli"
|
||||
|
||||
# Strip out semantic version metadata so it can be cast to `Version`
|
||||
[Version]$existingVersion, $null = (dotnet --version) -split " " -split "-"
|
||||
Assert ($existingVersion -ge [Version]("8.0")) ".NET SDK 8.0 or higher is required, please update it: https://aka.ms/dotnet-cli"
|
||||
|
||||
Write-Build DarkGreen "Using dotnet v$(dotnet --version) at path $((Get-Command dotnet).Source)"
|
||||
}
|
||||
|
||||
Task Clean FindDotNet, {
|
||||
Write-Build DarkMagenta "Cleaning PowerShellEditorServices"
|
||||
Invoke-BuildExec { & dotnet clean --verbosity $Verbosity }
|
||||
Remove-BuildItem module/PowerShellEditorServices/bin
|
||||
Remove-BuildItem module/PowerShellEditorServices/Commands/en-US/*-help.xml
|
||||
Remove-BuildItem module/PSReadLine
|
||||
Remove-BuildItem module/PSScriptAnalyzer
|
||||
}
|
||||
|
||||
Task CreateBuildInfo {
|
||||
$buildOrigin = "Development"
|
||||
$buildCommit = git rev-parse HEAD
|
||||
|
||||
[xml]$xml = Get-Content "PowerShellEditorServices.Common.props"
|
||||
$buildVersion = $xml.Project.PropertyGroup.VersionPrefix
|
||||
$prerelease = $xml.Project.PropertyGroup.VersionSuffix
|
||||
if ($prerelease) { $buildVersion += "-$prerelease" }
|
||||
|
||||
# Set build info fields on build platforms
|
||||
if ($env:TF_BUILD) { # Azure DevOps AKA OneBranch
|
||||
if ($env:BUILD_REASON -like "Manual") {
|
||||
$buildOrigin = "Release"
|
||||
} else {
|
||||
$buildOrigin = "AzureDevOps-CI"
|
||||
}
|
||||
} elseif ($env:GITHUB_ACTIONS) {
|
||||
$buildOrigin = "GitHub-CI"
|
||||
}
|
||||
|
||||
[string]$buildTime = [datetime]::Today.ToString("s", [System.Globalization.CultureInfo]::InvariantCulture)
|
||||
|
||||
$buildInfoContents = @"
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
public static class BuildInfo
|
||||
{
|
||||
public static readonly string BuildVersion = "$buildVersion";
|
||||
public static readonly string BuildOrigin = "$buildOrigin";
|
||||
public static readonly string BuildCommit = "$buildCommit";
|
||||
public static readonly System.DateTime? BuildTime = System.DateTime.Parse("$buildTime", CultureInfo.InvariantCulture.DateTimeFormat);
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
if (Compare-Object $buildInfoContents.Split([Environment]::NewLine) (Get-Content $script:BuildInfoPath)) {
|
||||
Write-Build DarkMagenta "Updating build info"
|
||||
Set-Content -LiteralPath $script:BuildInfoPath -Value $buildInfoContents -Force
|
||||
}
|
||||
}
|
||||
|
||||
task RestorePsesModules {
|
||||
# NOTE: When updating module versions, ensure they are also saved to the CFS feed
|
||||
if (-not (Test-Path "module/PSScriptAnalyzer")) {
|
||||
Write-Build DarkMagenta "Restoring PSScriptAnalyzer module"
|
||||
Save-PSResource -Path module -Name PSScriptAnalyzer -Version "1.24.0" -Repository $PSRepository -TrustRepository -Verbose
|
||||
}
|
||||
if (-not (Test-Path "module/PSReadLine")) {
|
||||
Write-Build DarkMagenta "Restoring PSReadLine module"
|
||||
Save-PSResource -Path module -Name PSReadLine -Version "2.4.5" -Repository $PSRepository -TrustRepository -Verbose
|
||||
}
|
||||
}
|
||||
|
||||
Task Build FindDotNet, CreateBuildInfo, RestorePsesModules, {
|
||||
Write-Build DarkGreen 'Building PowerShellEditorServices'
|
||||
Invoke-BuildExec { & dotnet publish $script:dotnetBuildArgs ./src/PowerShellEditorServices/PowerShellEditorServices.csproj -f $script:NetFramework.Standard }
|
||||
Invoke-BuildExec { & dotnet publish $script:dotnetBuildArgs ./src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj -f $script:NetFramework.PS74 }
|
||||
|
||||
if (-not $script:IsNix) {
|
||||
Invoke-BuildExec { & dotnet publish $script:dotnetBuildArgs ./src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj -f $script:NetFramework.PS51 }
|
||||
}
|
||||
} -If {
|
||||
$Null -eq $script:ChangesDetected -or $true -eq $script:ChangesDetected
|
||||
}
|
||||
|
||||
Task AssembleModule -After Build {
|
||||
Write-Build DarkGreen 'Assembling PowerShellEditorServices module'
|
||||
$psesOutputPath = './module/PowerShellEditorServices'
|
||||
$psesBinOutputPath = "$psesOutputPath/bin"
|
||||
$psesDepsPath = "$psesBinOutputPath/Common"
|
||||
$psesCoreHostPath = "$psesBinOutputPath/Core"
|
||||
$psesDeskHostPath = "$psesBinOutputPath/Desktop"
|
||||
|
||||
foreach ($dir in $psesDepsPath, $psesCoreHostPath, $psesDeskHostPath) {
|
||||
New-Item -Force -Path $dir -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
# Copy documents to module root
|
||||
foreach ($document in @('LICENSE', 'NOTICE.txt', 'README.md', 'SECURITY.md')) {
|
||||
Copy-Item -Force -Path $document -Destination './module'
|
||||
}
|
||||
|
||||
# Assemble PSES module
|
||||
$includedDlls = [System.Collections.Generic.HashSet[string]]::new()
|
||||
[void]$includedDlls.Add('System.Management.Automation.dll')
|
||||
|
||||
# PSES/bin/Common
|
||||
foreach ($psesComponent in Get-ChildItem $script:PsesOutput) {
|
||||
if ($psesComponent.Name -eq 'System.Management.Automation.dll' -or
|
||||
$psesComponent.Name -eq 'System.Runtime.InteropServices.RuntimeInformation.dll') {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($psesComponent.Extension) {
|
||||
[void]$includedDlls.Add($psesComponent.Name)
|
||||
Copy-Item -Path $psesComponent.FullName -Destination $psesDepsPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
# PSES/bin/Core
|
||||
foreach ($hostComponent in Get-ChildItem $script:HostCoreOutput) {
|
||||
if (-not $includedDlls.Contains($hostComponent.Name)) {
|
||||
Copy-Item -Path $hostComponent.FullName -Destination $psesCoreHostPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
# PSES/bin/Desktop
|
||||
if (-not $script:IsNix) {
|
||||
foreach ($hostComponent in Get-ChildItem $script:HostDeskOutput) {
|
||||
if (-not $includedDlls.Contains($hostComponent.Name)) {
|
||||
Copy-Item -Path $hostComponent.FullName -Destination $psesDeskHostPath -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task BuildCmdletHelp -After AssembleModule {
|
||||
Write-Build DarkGreen 'Building cmdlet help'
|
||||
New-ExternalHelp -Path ./module/docs -OutputPath ./module/PowerShellEditorServices/Commands/en-US -Force
|
||||
}
|
||||
|
||||
Task SetupHelpForTests {
|
||||
# Some CI do not ship with help included, and the secure devops pipeline also does not allow internet access, so we must update help from our local repository source.
|
||||
|
||||
# Only commands in Microsoft.PowerShell.Archive can be tested for help so as to minimize the repository storage.
|
||||
# This requires admin rights for PS5.1
|
||||
|
||||
# NOTE: You can run this task once as admin or update help separately, and continue to run tests as non-admin, if for instance developing locally.
|
||||
|
||||
$installHelpScript = {
|
||||
param(
|
||||
[Parameter(Position = 0)][string]$helpPath
|
||||
)
|
||||
$PSVersion = $PSVersionTable.PSVersion
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$helpPath = Resolve-Path $helpPath
|
||||
if ($PSEdition -ne 'Desktop') {
|
||||
$helpPath = Join-Path $helpPath '7'
|
||||
}
|
||||
|
||||
if ((Get-Help Expand-Archive).remarks -notlike 'Get-Help cannot find the Help files*') {
|
||||
Write-Host -ForegroundColor Green "PowerShell $PSVersion Archive help is already installed"
|
||||
return
|
||||
}
|
||||
|
||||
if ($PSEdition -eq 'Desktop') {
|
||||
# Cant use requires RunAsAdministrator because PS isn't smart enough to know this is a subscript.
|
||||
if (-not [Security.Principal.WindowsPrincipal]::new(
|
||||
[Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
).IsInRole(
|
||||
[Security.Principal.WindowsBuiltInRole]::Administrator
|
||||
)) {
|
||||
throw 'Windows PowerShell Update-Help requires admin rights. Please re-run the script in an elevated PowerShell session!'
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host -ForegroundColor Magenta "PowerShell $PSVersion Archive help is not installed, installing from $helpPath"
|
||||
|
||||
$updateHelpParams = @{
|
||||
Module = 'Microsoft.PowerShell.Archive'
|
||||
SourcePath = $helpPath
|
||||
UICulture = 'en-US'
|
||||
Force = $true
|
||||
Verbose = $true
|
||||
}
|
||||
|
||||
# PS7+ does not require admin rights if CurrentUser is used for scope. PS5.1 does not have this option.
|
||||
if ($PSEdition -ne 'Desktop') {
|
||||
$updateHelpParams.'Scope' = 'CurrentUser'
|
||||
}
|
||||
# Update the help and capture verbose output
|
||||
$updateHelpOutput = Update-Help @updateHelpParams *>&1
|
||||
|
||||
if ((Get-Help Expand-Archive).remarks -like 'Get-Help cannot find the Help files*') {
|
||||
throw "Failed to install PowerShell $PSVersion Help: $updateHelpOutput"
|
||||
} else {
|
||||
Write-Host -ForegroundColor Green "PowerShell $PSVersion Archive help installed successfully"
|
||||
}
|
||||
}
|
||||
|
||||
# Need this to inject the help file path since PSScriptRoot won't work inside the script
|
||||
$helpPath = Resolve-Path "$PSScriptRoot\test\PowerShellEditorServices.Test.Shared\PSHelp" -ErrorAction Stop
|
||||
Write-Build DarkMagenta "Runner help located at $helpPath"
|
||||
|
||||
if (Get-Command powershell.exe -CommandType Application -ea 0) {
|
||||
Write-Build DarkMagenta 'Checking PowerShell 5.1 help'
|
||||
& powershell.exe -NoProfile -NonInteractive -Command $installHelpScript -args $helpPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Failed to install PowerShell 5.1 help!'
|
||||
}
|
||||
}
|
||||
|
||||
if ($PwshPreview -and (Get-Command $PwshPreview -ea 0)) {
|
||||
Write-Build DarkMagenta "Checking PowerShell Preview help at $PwshPreview"
|
||||
Invoke-BuildExec { & $PwshPreview -NoProfile -NonInteractive -Command $installHelpScript -args $helpPath }
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Failed to install PowerShell Preview help!'
|
||||
}
|
||||
}
|
||||
|
||||
if ($PSEdition -eq 'Core') {
|
||||
Write-Build DarkMagenta "Checking this PowerShell process's help"
|
||||
& $installHelpScript $helpPath
|
||||
}
|
||||
}
|
||||
|
||||
Task TestPS74 Build, SetupHelpForTests, {
|
||||
Set-Location ./test/PowerShellEditorServices.Test/
|
||||
Invoke-BuildExec { & dotnet $script:dotnetTestArgs $script:NetFramework.PS74 }
|
||||
}
|
||||
|
||||
Task TestPS51 -If (-not $script:IsNix) Build, SetupHelpForTests, {
|
||||
Set-Location ./test/PowerShellEditorServices.Test/
|
||||
# TODO: See https://github.com/dotnet/sdk/issues/18353 for x64 test host
|
||||
# that is debuggable! If architecture is added, the assembly path gets an
|
||||
# additional folder, necessitating fixes to find the commands definition
|
||||
# file and test files.
|
||||
try {
|
||||
# TODO: See https://github.com/PowerShell/vscode-powershell/issues/3886
|
||||
# Inheriting the module path for powershell.exe breaks things!
|
||||
$originalModulePath = $env:PSModulePath
|
||||
$env:PSModulePath = ''
|
||||
Invoke-BuildExec { & dotnet $script:dotnetTestArgs $script:NetFramework.PS51 }
|
||||
} finally {
|
||||
$env:PSModulePath = $originalModulePath
|
||||
}
|
||||
}
|
||||
|
||||
# NOTE: The framework for the E2E tests applies to the mock client, and so
|
||||
# should just be the latest supported framework.
|
||||
Task TestE2EPwsh Build, SetupHelpForTests, {
|
||||
Set-Location ./test/PowerShellEditorServices.Test.E2E/
|
||||
$env:PWSH_EXE_NAME = 'pwsh'
|
||||
Invoke-BuildExec { & dotnet $script:dotnetTestArgs $script:NetFramework.PS74 }
|
||||
}
|
||||
|
||||
if ($env:GITHUB_ACTIONS) {
|
||||
$PwshPreview = if ($script:IsNix) { "$PSScriptRoot/preview/pwsh" } else { "$PSScriptRoot/preview/pwsh.exe" }
|
||||
} else {
|
||||
$PwshPreview = if ($script:IsNix) { "$HOME/.powershell-preview/pwsh" } else { "$env:LOCALAPPDATA/Microsoft/powershell-preview/pwsh.exe" }
|
||||
}
|
||||
|
||||
Task TestE2EPreview -If (-not $env:TF_BUILD) Build, SetupHelpForTests, {
|
||||
Assert (Test-Path $PwshPreview) "PowerShell Preview not found at $PwshPreview, please install it: https://github.com/PowerShell/PowerShell/blob/master/tools/install-powershell.ps1"
|
||||
Set-Location ./test/PowerShellEditorServices.Test.E2E/
|
||||
$env:PWSH_EXE_NAME = $PwshPreview
|
||||
Write-Build DarkGreen "Running end-to-end tests with: $(& $PwshPreview --version)"
|
||||
Invoke-BuildExec { & dotnet $script:dotnetTestArgs $script:NetFramework.PS74 }
|
||||
}
|
||||
|
||||
Task TestE2EPowerShell -If (-not $script:IsNix) Build, SetupHelpForTests, {
|
||||
Set-Location ./test/PowerShellEditorServices.Test.E2E/
|
||||
$env:PWSH_EXE_NAME = 'powershell'
|
||||
try {
|
||||
# TODO: See https://github.com/PowerShell/vscode-powershell/issues/3886
|
||||
# Inheriting the module path for powershell.exe breaks things!
|
||||
$originalModulePath = $env:PSModulePath
|
||||
$env:PSModulePath = ''
|
||||
Invoke-BuildExec { & dotnet $script:dotnetTestArgs $script:NetFramework.PS74 }
|
||||
} finally {
|
||||
$env:PSModulePath = $originalModulePath
|
||||
}
|
||||
}
|
||||
|
||||
Task TestE2EPwshCLM -If (-not $script:IsNix) Build, SetupHelpForTests, {
|
||||
Set-Location ./test/PowerShellEditorServices.Test.E2E/
|
||||
$env:PWSH_EXE_NAME = 'pwsh'
|
||||
|
||||
if (-not [Security.Principal.WindowsIdentity]::GetCurrent().Owner.IsWellKnown('BuiltInAdministratorsSid')) {
|
||||
Write-Build DarkRed 'Skipping Constrained Language Mode tests as they must be ran in an elevated process'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Build DarkGreen 'Running end-to-end tests in Constrained Language Mode'
|
||||
[System.Environment]::SetEnvironmentVariable('__PSLockdownPolicy', '0x80000007', [System.EnvironmentVariableTarget]::Machine)
|
||||
Invoke-BuildExec { & dotnet $script:dotnetTestArgs $script:NetFramework.PS74 }
|
||||
} finally {
|
||||
[System.Environment]::SetEnvironmentVariable('__PSLockdownPolicy', $null, [System.EnvironmentVariableTarget]::Machine)
|
||||
}
|
||||
}
|
||||
|
||||
Task TestE2EPowerShellCLM -If (-not $script:IsNix) Build, SetupHelpForTests, {
|
||||
Set-Location ./test/PowerShellEditorServices.Test.E2E/
|
||||
$env:PWSH_EXE_NAME = 'powershell'
|
||||
|
||||
if (-not [Security.Principal.WindowsIdentity]::GetCurrent().Owner.IsWellKnown('BuiltInAdministratorsSid')) {
|
||||
Write-Build DarkRed 'Skipping Constrained Language Mode tests as they must be ran in an elevated process'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Build DarkGreen 'Running end-to-end tests in Constrained Language Mode'
|
||||
[System.Environment]::SetEnvironmentVariable('__PSLockdownPolicy', '0x80000007', [System.EnvironmentVariableTarget]::Machine)
|
||||
# TODO: See https://github.com/PowerShell/vscode-powershell/issues/3886
|
||||
# Inheriting the module path for powershell.exe breaks things!
|
||||
$originalModulePath = $env:PSModulePath
|
||||
$env:PSModulePath = ''
|
||||
Invoke-BuildExec { & dotnet $script:dotnetTestArgs $script:NetFramework.PS74 }
|
||||
} finally {
|
||||
[System.Environment]::SetEnvironmentVariable('__PSLockdownPolicy', $null, [System.EnvironmentVariableTarget]::Machine)
|
||||
$env:PSModulePath = $originalModulePath
|
||||
}
|
||||
}
|
||||
|
||||
Task BuildIfChanged.Init -Before BuildIfChanged {
|
||||
[bool]$script:ChangesDetected = $false
|
||||
}
|
||||
|
||||
Task BuildIfChanged -Inputs {
|
||||
$slash = [IO.Path]::DirectorySeparatorChar
|
||||
Get-ChildItem ./src -Filter '*.cs' -Recurse
|
||||
| Where-Object FullName -NotLike ('*' + $slash + 'obj' + $slash + '*')
|
||||
| Where-Object FullName -NotLike ('*' + $slash + 'bin' + $slash + '*')
|
||||
} -Outputs {
|
||||
'./src/PowerShellEditorServices/bin/Debug/netstandard2.0/Microsoft.PowerShell.EditorServices.dll'
|
||||
'./src/PowerShellEditorServices.Hosting/bin/Debug/net8.0/Microsoft.PowerShell.EditorServices.Hosting.dll'
|
||||
} -Jobs {
|
||||
Write-Build DarkMagenta 'Changes detected, rebuilding'
|
||||
$script:ChangesDetected = $true
|
||||
}, Build
|
||||
|
||||
Task Test TestPS74, TestE2EPwsh, TestPS51, TestE2EPowerShell
|
||||
|
||||
Task TestFull Test, TestE2EPreview, TestE2EPwshCLM, TestE2EPowerShellCLM
|
||||
|
||||
Task . Clean, Build, Test
|
||||
57
PowerShellEditorServices.sln
Normal file
57
PowerShellEditorServices.sln
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F46EF430-95AA-4386-9259-292A443AB715}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Test.Shared", "test\PowerShellEditorServices.Test.Shared\PowerShellEditorServices.Test.Shared.csproj", "{9D307AF9-D1F7-4185-AE9B-2DD3F178832C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Test", "test\PowerShellEditorServices.Test\PowerShellEditorServices.Test.csproj", "{DFD3C9C2-F9E6-4EE3-B614-A8EA7D1E1A98}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Test.E2E", "test\PowerShellEditorServices.Test.E2E\PowerShellEditorServices.Test.E2E.csproj", "{AA007633-5178-4D73-A262-CCE7247BDE93}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F40C4EC9-AE86-4A26-974F-95381888DCDC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices", "src\PowerShellEditorServices\PowerShellEditorServices.csproj", "{B4431254-9A2F-43DE-A998-12B22A1593CE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Hosting", "src\PowerShellEditorServices.Hosting\PowerShellEditorServices.Hosting.csproj", "{983D05F2-3C77-4B51-9A28-A8C6595911BA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{9D307AF9-D1F7-4185-AE9B-2DD3F178832C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9D307AF9-D1F7-4185-AE9B-2DD3F178832C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9D307AF9-D1F7-4185-AE9B-2DD3F178832C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9D307AF9-D1F7-4185-AE9B-2DD3F178832C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DFD3C9C2-F9E6-4EE3-B614-A8EA7D1E1A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DFD3C9C2-F9E6-4EE3-B614-A8EA7D1E1A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DFD3C9C2-F9E6-4EE3-B614-A8EA7D1E1A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DFD3C9C2-F9E6-4EE3-B614-A8EA7D1E1A98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AA007633-5178-4D73-A262-CCE7247BDE93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AA007633-5178-4D73-A262-CCE7247BDE93}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AA007633-5178-4D73-A262-CCE7247BDE93}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AA007633-5178-4D73-A262-CCE7247BDE93}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B4431254-9A2F-43DE-A998-12B22A1593CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B4431254-9A2F-43DE-A998-12B22A1593CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B4431254-9A2F-43DE-A998-12B22A1593CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B4431254-9A2F-43DE-A998-12B22A1593CE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{983D05F2-3C77-4B51-9A28-A8C6595911BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{983D05F2-3C77-4B51-9A28-A8C6595911BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{983D05F2-3C77-4B51-9A28-A8C6595911BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{983D05F2-3C77-4B51-9A28-A8C6595911BA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{9D307AF9-D1F7-4185-AE9B-2DD3F178832C} = {F46EF430-95AA-4386-9259-292A443AB715}
|
||||
{DFD3C9C2-F9E6-4EE3-B614-A8EA7D1E1A98} = {F46EF430-95AA-4386-9259-292A443AB715}
|
||||
{AA007633-5178-4D73-A262-CCE7247BDE93} = {F46EF430-95AA-4386-9259-292A443AB715}
|
||||
{B4431254-9A2F-43DE-A998-12B22A1593CE} = {F40C4EC9-AE86-4A26-974F-95381888DCDC}
|
||||
{983D05F2-3C77-4B51-9A28-A8C6595911BA} = {F40C4EC9-AE86-4A26-974F-95381888DCDC}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
234
README.md
Normal file
234
README.md
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
# PowerShell Editor Services
|
||||
|
||||
[](https://github.com/PowerShell/PowerShellEditorServices/actions/workflows/ci-test.yml)
|
||||
[](https://aka.ms/psdiscord)
|
||||
[](https://gitter.im/PowerShell/PowerShellEditorServices?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
**PowerShell Editor Services** is a PowerShell module that provides common
|
||||
functionality needed to enable a consistent and robust PowerShell development
|
||||
experience in almost any editor or integrated development environment (IDE).
|
||||
|
||||
## [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) clients using PowerShell Editor Services:
|
||||
|
||||
- [PowerShell for Visual Studio Code](https://github.com/PowerShell/vscode-powershell)
|
||||
> [!NOTE]
|
||||
> PowerShell for Azure Data Studio will no longer be updated or maintained.
|
||||
|
||||
The functionality in PowerShell Editor Services is available in the following editor extensions:
|
||||
> [!WARNING]
|
||||
> These clients are community maintained and may be very out of date.
|
||||
It is recommended to use a generic [LSP plugin](#Usage) with your client if possible.
|
||||
|
||||
- [lsp-pwsh](https://github.com/emacs-lsp/lsp-mode/blob/master/clients/lsp-pwsh.el), an Emacs PowerShell plugin
|
||||
- [intellij-powershell](https://github.com/ant-druha/intellij-powershell), adds PowerShell language support to IntelliJ-based IDEs
|
||||
- [coc-powershell](https://github.com/yatli/coc-powershell), a Vim and Neovim plugin
|
||||
- [powershell.nvim](https://github.com/TheLeoP/powershell.nvim) a Neovim plugin
|
||||
|
||||
## Supported PowerShell Versions
|
||||
|
||||
PSES runs as a PowerShell Module in [currently supported versions of PowerShell 7+](https://learn.microsoft.com/en-us/powershell/scripting/install/powershell-support-lifecycle).
|
||||
|
||||
Windows PowerShell 5.1 is supported on a best-effort basis.
|
||||
|
||||
## Features
|
||||
|
||||
- The Language Service provides common editor features for the PowerShell language:
|
||||
- Code navigation actions (find references, go to definition)
|
||||
- Statement completions (IntelliSense)
|
||||
- Real-time semantic analysis of scripts using PowerShell Script Analyzer
|
||||
- The Debugging Service simplifies interaction with the PowerShell debugger (breakpoints, variables, call stack, etc.)
|
||||
- The [$psEditor API](docs/guide/extensions.md) enables scripting of the host editor
|
||||
- A full, Extension Terminal experience for interactive development and debugging
|
||||
|
||||
## Usage
|
||||
|
||||
If you're looking to integrate PowerShell Editor Services into your [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) compliant editor or client,
|
||||
we support two ways of connecting.
|
||||
|
||||
### Named Pipes / Unix Domain Sockets
|
||||
|
||||
If you're looking for a more feature-rich experience,
|
||||
named pipes (AKA sockets) are the way to go.
|
||||
They give you all the benefits of the Language Server Protocol with extra capabilities that you can take advantage of:
|
||||
|
||||
- The PowerShell Extension Terminal
|
||||
- Debugging using the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/)
|
||||
|
||||
The typical command to start PowerShell Editor Services using named pipes / sockets is as follows:
|
||||
|
||||
```powershell
|
||||
pwsh -NoLogo -NoProfile -Command "./PowerShellEditorServices/Start-EditorServices.ps1 -SessionDetailsPath ./session.json"
|
||||
```
|
||||
|
||||
The start script, `Start-EditorServices.ps1`, is found in the `PowerShellEditorServices` folder instead the `PowerShellEditorServices.zip` downloaded from the GitHub releases.
|
||||
|
||||
The session details (which named pipes were created) will be written to the given session details path,
|
||||
and the client needs to point to these in order to connect.
|
||||
|
||||
The Visual Studio Code, Vim, Neovim, and IntelliJ extensions use named pipes.
|
||||
|
||||
### Standard Input and Output
|
||||
|
||||
Alternatively, the `-SessionDetailsPath ./session.json` argument can be replaced with just `-Stdio`.
|
||||
The use of stdio is the _simplest_ way to connect with most LSP clients,
|
||||
but will limit some features, such as the debugger and Extension Terminal.
|
||||
This is because because these two features require their own IO streams and stdio only provides a single pair of streams.
|
||||
|
||||
Please see the [emacs-simple-test.el](test/emacs-simple-test.el),
|
||||
[emacs-test.el](test/emacs-test.el),
|
||||
[vim-simple-test.vim](test/vim-simple-test.vim) and [vim-test.vim](test/vim-test.vim) for examples of end-to-end tested configurations.
|
||||
They use [eglot for Emacs](https://github.com/joaotavora/eglot) and [LanguageClient-neovim](https://github.com/autozimu/LanguageClient-neovim).
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
If you are trying to automate the service in PowerShell, you can also run it under `Start-Process` to prevent hanging your script.
|
||||
It also gives you access to process automation features like `$process.Close()` or `$process.Kill()`.
|
||||
The `Start-EditorServices.ps1` script takes many more optional arguments, but they no longer _need_ to be specified.
|
||||
|
||||
```powershell
|
||||
$command = @(
|
||||
"$PSES_BUNDLE_PATH/PowerShellEditorServices/Start-EditorServices.ps1",
|
||||
"-BundledModulesPath $PSES_BUNDLE_PATH",
|
||||
"-LogPath ./logs",
|
||||
"-SessionDetailsPath ./session.json",
|
||||
"-FeatureFlags @()",
|
||||
"-AdditionalModules @()",
|
||||
"-HostName 'My Client'",
|
||||
"-HostProfileId 'myclient'",
|
||||
"-HostVersion 1.0.0",
|
||||
"-LogLevel Trace"
|
||||
) -join " "
|
||||
|
||||
$pwsh_arguments = "-NoLogo -NoProfile -Command $command"
|
||||
$process = Start-Process pwsh -ArgumentList $arguments -PassThru
|
||||
...
|
||||
$process.Close(); #$process.Kill();
|
||||
```
|
||||
|
||||
Once the command is run,
|
||||
PowerShell Editor Services will wait until the client connects to the named pipe.
|
||||
The `session.json` will contain the paths of the named pipes that you will connect to.
|
||||
There will be one you immediately connect to for Language Server Protocol messages,
|
||||
and once you connect to when you launch the debugger for Debug Adapter Protocol messages.
|
||||
|
||||
### PowerShell Extension Terminal
|
||||
|
||||

|
||||
|
||||
The PowerShell Extension Terminal uses the host process' stdio streams for console input and output.
|
||||
Please note that this is mutually exclusive from using stdio for the Language Server Protocol messages.
|
||||
|
||||
If you want to take advantage of the PowerShell Extension Terminal,
|
||||
you must include the `-EnableConsoleRepl` switch when calling `Start-EditorServices.ps1`.
|
||||
|
||||
This is typically used if your client can create arbitrary terminals in the editor like below:
|
||||
|
||||

|
||||
|
||||
The Visual Studio Code, Vim, and IntelliJ extensions currently use the PowerShell Extension Terminal.
|
||||
|
||||
### Debugging
|
||||
|
||||
Debugging support is also exposed with PowerShell Editor Services.
|
||||
It is handled within the same process as the Language Server Protocol.
|
||||
This provides a more integrated experience for end users but is something to note as not many other language servers work in the same way.
|
||||
If you want to take advantage of debugging,
|
||||
your client must support the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/).
|
||||
Your client should use the path to the debug named pipe found in the `session.json` file talked about above.
|
||||
|
||||
The debugging functionality in PowerShell Editor Services is available in the following editor extensions:
|
||||
|
||||
- [PowerShell for Visual Studio Code](https://github.com/PowerShell/vscode-powershell)
|
||||
- [nvim-dap-powershell for Neovim](https://github.com/Willem-J-an/nvim-dap-powershell)
|
||||
- [powershell.nvim for Neovim](https://github.com/TheLeoP/powershell.nvim)
|
||||
- [intellij-powershell](https://github.com/ant-druha/intellij-powershell)
|
||||
|
||||
## API Usage
|
||||
|
||||
Please note that we only consider the following as stable APIs that can be relied on:
|
||||
|
||||
- Language Server Protocol connection
|
||||
- Debug Adapter Protocol connection
|
||||
- Start-up mechanism
|
||||
|
||||
The types of PowerShell Editor Services can change at any moment and should not be linked against in a production environment.
|
||||
|
||||
## Development Environment
|
||||
|
||||
> [!TIP]
|
||||
> The easiest way to manually test changes you've made in PowerShellEditorServices is to follow the [vscode-powershell development doc](https://github.com/PowerShell/vscode-powershell/blob/main/docs/development.md).
|
||||
|
||||
### 1. Install PowerShell 7+
|
||||
|
||||
Install PowerShell 7+ with [these instructions](https://github.com/PowerShell/PowerShell#get-powershell).
|
||||
|
||||
### 2. Clone the GitHub repository
|
||||
|
||||
```powershell
|
||||
git clone https://github.com/PowerShell/PowerShellEditorServices.git
|
||||
```
|
||||
|
||||
### 3. Install [Invoke-Build](https://github.com/nightroman/Invoke-Build)
|
||||
|
||||
```powershell
|
||||
Install-Module InvokeBuild -Scope CurrentUser
|
||||
Install-Module platyPS -Scope CurrentUser
|
||||
```
|
||||
|
||||
### 4. Adjust `nuget.config` if necessary
|
||||
|
||||
Our NuGet configuration uses a secure feed with allow-listed third party dependency packages. If your contribution requires any changes to the included NuGet packages, you must disable this secure feed.
|
||||
|
||||
First, run this command to prevent accidentally commiting changes to this file
|
||||
|
||||
```powershell
|
||||
git update-index --skip-worktree nuget.config
|
||||
```
|
||||
|
||||
Then, either delete the file or remove the `packagesources` section to use nuget.org again. Your PR _will_ fail automated build checks and you _must_ inform us at the top of your PR so the appropriate packages can be added if approved.
|
||||
|
||||
## Build PowerShell Editor Services
|
||||
Now you're ready to build the code. You can do so in one of two ways:
|
||||
|
||||
### PowerShell
|
||||
|
||||
```powershell
|
||||
PS C:\src\PowerShellEditorServices> Invoke-Build
|
||||
```
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
Open the PowerShellEditorServices folder that you cloned locally and press <kbd>Ctrl+Shift+B</kbd>
|
||||
(or <kbd>Cmd+Shift+B</kbd> on macOS) which will run the default build task.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please see our [Code of Conduct](CODE_OF_CONDUCT.md) before participating in this project.
|
||||
|
||||
## Contributions Welcome
|
||||
|
||||
We would love to incorporate community contributions into this project. If you would like to
|
||||
contribute code, documentation, tests, or bug reports, please read our [Contribution Guide](CONTRIBUTING.md) to learn more.
|
||||
|
||||
## Security Note
|
||||
|
||||
For any security issues, please see [here](SECURITY.md).
|
||||
|
||||
## Maintainers
|
||||
|
||||
- Andy Jordan - [@andyleejordan](https://github.com/andyleejordan)
|
||||
- Patrick Meinecke - [@SeeminglyScience](https://github.com/SeeminglyScience)
|
||||
- Sydney Smith - [@SydneyhSmith](https://github.com/SydneyhSmith)
|
||||
- Justin Grote - [@JustinGrote](https://github.com/JustinGrote)
|
||||
|
||||
### Emeriti
|
||||
|
||||
- Rob Holt - [@rjmholt](https://github.com/rjmholt)
|
||||
- Tyler Leonhardt - [@TylerLeonhardt](https://github.com/TylerLeonhardt)
|
||||
- David Wilson - [@daviwil](https://github.com/daviwil)
|
||||
|
||||
## License
|
||||
|
||||
This project is [licensed under the MIT License](LICENSE). Please see the
|
||||
[third-party notices](NOTICE.txt) file for details on the third-party
|
||||
binaries that we include with releases of this project.
|
||||
41
SECURITY.md
Normal file
41
SECURITY.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
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).
|
||||
|
||||
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).
|
||||
|
||||
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).
|
||||
|
||||
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:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
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.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
||||
68
default.nix
Normal file
68
default.nix
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
pkgs ? import <nixpkgs> { },
|
||||
}:
|
||||
let
|
||||
psreadline = pkgs.fetchzip {
|
||||
url = "https://www.powershellgallery.com/api/v2/package/PSReadLine/2.4.5";
|
||||
sha256 = "12pwy5b426lv7cmlasa7ajy202a0vjjs9drfyg2q5zwqxhj8g8cz";
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
psscriptanalyzer = pkgs.fetchzip {
|
||||
url = "https://www.powershellgallery.com/api/v2/package/PSScriptAnalyzer/1.24.0";
|
||||
sha256 = "03bz9f5rcx749758h6l9wsv86300s0a34qgd522v6166crbpvg2p";
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
invokebuild = pkgs.fetchzip {
|
||||
url = "https://www.powershellgallery.com/api/v2/package/InvokeBuild/5.12.2";
|
||||
sha256 = "0cry70c6ndlv4y0fmlg5iip0ix1r51p7i6dxx3y2zzczbkz2mn6m";
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
platyps = pkgs.fetchzip {
|
||||
url = "https://www.powershellgallery.com/api/v2/package/platyPS/0.14.2";
|
||||
sha256 = "13m11mf42blcn2327m9a4dqfg8sgjjg1vg9dmiwvnbq7wpam260q";
|
||||
stripRoot = false;
|
||||
};
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "powershell-editor-services";
|
||||
version = "4.4.0";
|
||||
src = ./.;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.dotnet-sdk_8
|
||||
pkgs.git
|
||||
pkgs.powershell
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
export HOME=$TMPDIR
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
export NUGET_PACKAGES=$TMPDIR/nuget
|
||||
|
||||
git init -q
|
||||
git config user.email "nix@local"
|
||||
git config user.name "nix"
|
||||
git add -A
|
||||
git commit -qm "nix build"
|
||||
|
||||
mkdir -p module
|
||||
cp -r ${psreadline} module/PSReadLine
|
||||
cp -r ${psscriptanalyzer} module/PSScriptAnalyzer
|
||||
|
||||
mkdir -p $TMPDIR/psmodules
|
||||
cp -r ${invokebuild} $TMPDIR/psmodules/InvokeBuild
|
||||
cp -r ${platyps} $TMPDIR/psmodules/platyPS
|
||||
export PSModulePath="$TMPDIR/psmodules${"PSModulePath:+:$PSModulePath"}"
|
||||
|
||||
pwsh -NoLogo -NoProfile -Command \
|
||||
"Import-Module InvokeBuild; Invoke-Build -File PowerShellEditorServices.build.ps1 -Task AssembleModule -Configuration Release"
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/share/powershell/Modules
|
||||
cp -r module/* $out/share/powershell/Modules/
|
||||
'';
|
||||
}
|
||||
28
docs/api/index.md
Normal file
28
docs/api/index.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# API Reference
|
||||
|
||||
The .NET API for PowerShell Editor Services is organized in a way that allows
|
||||
you to easily get started using all of its services but also giving you the
|
||||
option to only use the services you care about in your application.
|
||||
|
||||
The best starting point is the @Microsoft.PowerShell.EditorServices.EditorSession
|
||||
class which can start up all of the following services for use in a single editing
|
||||
session.
|
||||
|
||||
Use the @Microsoft.PowerShell.EditorServices.LanguageService to provide language
|
||||
intelligence behaviors like finding the references or definition of a cmdlet or variable.
|
||||
|
||||
Use the @Microsoft.PowerShell.EditorServices.AnalysisService to provide rule-based
|
||||
analysis of scripts using [PowerShell Script Analyzer](https://github.com/PowerShell/PSScriptAnalyzer).
|
||||
|
||||
Use the @Microsoft.PowerShell.EditorServices.DebugService to easily interact with
|
||||
the PowerShell debugger.
|
||||
|
||||
Use the @Microsoft.PowerShell.EditorServices.Console.ConsoleService to provide interactive
|
||||
console support in the user's editor.
|
||||
|
||||
Use the @Microsoft.PowerShell.EditorServices.Services.ExtensionService to allow
|
||||
the user to extend the host editor with new capabilities using PowerShell code.
|
||||
|
||||
The core of all the services is the @Microsoft.PowerShell.EditorServices.PowerShellContext
|
||||
class. This class manages a session's runspace and handles script and command
|
||||
execution no matter what state the runspace is in.
|
||||
69
docs/docfx.json
Normal file
69
docs/docfx.json
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"metadata": [
|
||||
{
|
||||
"src": [
|
||||
{
|
||||
"files": [ "*.csproj" ],
|
||||
"cwd": "../src/PowerShellEditorServices",
|
||||
"exclude": [ "**/obj/**", "**/bin/**" ]
|
||||
}
|
||||
],
|
||||
"dest": "metadata/api"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"content": [
|
||||
{
|
||||
"cwd": "metadata/api",
|
||||
"files": [
|
||||
"**/**.yml"
|
||||
],
|
||||
"dest": "api"
|
||||
},
|
||||
{
|
||||
"cwd": "../",
|
||||
"files": [
|
||||
"CONTRIBUTING.md",
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cwd": ".",
|
||||
"files": [
|
||||
"toc.yml",
|
||||
"index.md",
|
||||
"api/index.md",
|
||||
"guide/**.md"
|
||||
],
|
||||
"exclude": [
|
||||
"metadata/**",
|
||||
"_site/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"resource": [
|
||||
{
|
||||
"files": [
|
||||
"images/**"
|
||||
],
|
||||
"exclude": [
|
||||
"obj/**",
|
||||
"_site/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"overwrite": [
|
||||
{
|
||||
"files": [
|
||||
"apidoc/**.md"
|
||||
],
|
||||
"exclude": [
|
||||
"obj/**",
|
||||
"_site/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dest": "_site",
|
||||
"template": [ "default", "template" ]
|
||||
}
|
||||
}
|
||||
168
docs/guide/extensions.md
Normal file
168
docs/guide/extensions.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# Extending the Host Editor
|
||||
|
||||
PowerShell Editor Services exposes a common extensibility model which allows
|
||||
you to write extension code in PowerShell that works across any editor that
|
||||
uses PowerShell Editor Services.
|
||||
|
||||
## API Overview
|
||||
|
||||
### Introducing `$psEditor`
|
||||
|
||||
The entry point for the PowerShell Editor Services extensibility model is the `$psEditor`
|
||||
object of the type @Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorObject. For
|
||||
those familiar with the PowerShell ISE's `$psISE` object, the `$psEditor` object is very
|
||||
similar. The primary difference is that this model has been generalized to work against
|
||||
any editor which leverages PowerShell Editor Services for its PowerShell editing experience.
|
||||
|
||||
> NOTE: For now the `$psEditor` object is limited as it has just been
|
||||
> introduced. If you have ideas for other useful APIs it could expose
|
||||
> please file an issue on our GitHub page.
|
||||
|
||||
This object gives access to all of the high-level services in the current
|
||||
editing session. For example, the @Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorObject.Workspace
|
||||
property gives access to the editor's workspace, allowing you to create or open files
|
||||
in the editor.
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Opening a file in the editor
|
||||
|
||||
```powershell
|
||||
# Open the current user's profile for this editor
|
||||
$psEditor.Workspace.OpenFile($profile)
|
||||
```
|
||||
|
||||
#### Manipulating the user's active file buffer
|
||||
|
||||
```powershell
|
||||
# Insert new text replacing the user's current selection
|
||||
$context = $psEditor.GetEditorContext()
|
||||
$context.CurrentFile.InsertText("# All your script are belong to us", $context.SelectedRange)
|
||||
```
|
||||
|
||||
#### Setting the selection based on the cursor position
|
||||
|
||||
```powershell
|
||||
# Set the selection from their cursor position to the end of the same line
|
||||
$context = $psEditor.GetEditorContext()
|
||||
$context.SetSelection($context.CursorPosition, $context.CursorPosition.GetLineEnd())
|
||||
```
|
||||
|
||||
## Registering Editor Commands
|
||||
|
||||
The `$psEditor` object gives you the ability to write a script that can automate the
|
||||
host editor when run inside of it. However, you may not want to give a user a plain
|
||||
script that performs some operation. What if you'd prefer to add a new command to the
|
||||
editor which can execute your code when the user invokes it? The `Register-EditorCommand`
|
||||
cmdlet allows you to register either a function, cmdlet, or ScriptBlock as a
|
||||
command in the host editor.
|
||||
|
||||
### Registering a cmdlet or function command
|
||||
|
||||
```powershell
|
||||
function Invoke-MyCommand {
|
||||
Write-Output "My command's function was invoked!"
|
||||
}
|
||||
|
||||
Register-EditorCommand `
|
||||
-Name "MyModule.MyCommandWithFunction" `
|
||||
-DisplayName "My command with function" `
|
||||
-Function Invoke-MyCommand
|
||||
```
|
||||
|
||||
### Registering a script block command
|
||||
|
||||
```powershell
|
||||
Register-EditorCommand `
|
||||
-Name "MyModule.MyCommandWithScriptBlock" `
|
||||
-DisplayName "My command with script block" `
|
||||
-ScriptBlock { Write-Output "My command's script block was invoked!" }
|
||||
```
|
||||
|
||||
### The @Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext parameter
|
||||
|
||||
Your function, cmdlet, or ScriptBlock can optionally accept a single parameter
|
||||
of type @Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext which provides
|
||||
information about the state of the host editor at the time your command was
|
||||
invoked. With this object you can easily perform operations like manipulatin the
|
||||
state of the user's active editor buffer or changing the current selection.
|
||||
|
||||
The usual convention is that a `$context` parameter is added to your editor
|
||||
command's function. For now it is recommended that you fully specify the
|
||||
type of the @Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext object
|
||||
so that you get full IntelliSense on your context parameter.
|
||||
|
||||
Here is an example of using the `$context` parameter:
|
||||
|
||||
```powershell
|
||||
Register-EditorCommand `
|
||||
-Name "MyModule.MyEditorCommandWithContext" `
|
||||
-DisplayName "My command with context usage" `
|
||||
-ScriptBlock {
|
||||
param([Microsoft.PowerShell.EditorServices.Services.PowerShellContext.EditorContext]$context)
|
||||
Write-Output "The user's cursor is on line $($context.CursorPosition.Line)!"
|
||||
}
|
||||
```
|
||||
|
||||
### Suppressing command output
|
||||
|
||||
If you would like for your editor command to run without its output being
|
||||
written to the user's console, you can use the `-SuppressOutput` switch
|
||||
parameter of the `Register-EditorCommand` cmdlet. We recommend that you
|
||||
use this parameter if your command does not need to write output to the
|
||||
user's console.
|
||||
|
||||
Regardless of whether the `-SuppressOutput` parameter is used, any errors
|
||||
that occur while running your editor command will be written to the user's
|
||||
console.
|
||||
|
||||
## Using Editor Commands
|
||||
|
||||
If you've registered an editor command, either through your own code or
|
||||
a module that you've installed, you can launch it using your editor's **Show
|
||||
additional commands from PowerShell modules** command. Running this command
|
||||
will cause a list of commands to be displayed.
|
||||
|
||||
In Visual Studio Code, press `Ctrl+Shift+P` to open the command palette. Type
|
||||
the characters `addi` until you see the following item and then press `Enter`:
|
||||
|
||||

|
||||
|
||||
The list that appears next will show all of the editor commands that have
|
||||
been registered with PowerShell code. Selecting one of them will cause its
|
||||
function or ScriptBlock to be executed.
|
||||
|
||||

|
||||
|
||||
Other editors should follow a similar pattern, exposing this command list through
|
||||
a "Show additional commands" item in the command palette.
|
||||
|
||||
> NOTE: In the future we hope to be able to register editor commands at the top level
|
||||
> so that these commands are easier to find and so that they also can be bound to
|
||||
> hotkeys for quick access.
|
||||
|
||||
## Shipping an Extension Module
|
||||
|
||||
You can easily ship a module containing editor commands which get registered
|
||||
if the module is loaded into an editor session. Assuming that you've exported
|
||||
a function or cmdlet named `Invoke-MyEditorCommand` in your module's psd1
|
||||
file, you can add this code at the very end of your module's psm1 file:
|
||||
|
||||
```powershell
|
||||
if ($psEditor) {
|
||||
Register-EditorCommand `
|
||||
-Name "MyModule.MyEditorCommand" `
|
||||
-DisplayName "My editor command" `
|
||||
-Function Invoke-MyEditorCommand `
|
||||
-SuppressOutput
|
||||
}
|
||||
```
|
||||
|
||||
The user will now be able to import your module in their host editor's profile and
|
||||
your editor command will be immediately available after the PowerShell extension
|
||||
in that editor starts up.
|
||||
|
||||
> NOTE: In the future we plan to provide an easy way for the user to opt-in
|
||||
> to the automatic loading of any editor command modules that they've installed
|
||||
> from the PowerShell Gallery. If this interests you, please let us know on
|
||||
> [this GitHub issue](https://github.com/PowerShell/PowerShellEditorServices/issues/215).
|
||||
166
docs/guide/getting_started.md
Normal file
166
docs/guide/getting_started.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# Getting Started
|
||||
The PowerShell Editor Services project provides a Language Server Protocol (LSP)
|
||||
HTTP server that runs outside the editor. The server supplies rich editor
|
||||
functionality like code completion, syntax highlighting, and code annotation.
|
||||
This document will guide you through getting a minimal setup working with
|
||||
several editors.
|
||||
|
||||
## Editors
|
||||
1. [Neovim](#neovim)
|
||||
|
||||
## Neovim
|
||||
|
||||
### Install the Server
|
||||
Download and extract the PowerShell Editor Services server from the
|
||||
[releases page](https://github.com/PowerShell/PowerShellEditorServices/releases)
|
||||
into a directory of your choice. Remember the path that you extract the
|
||||
project into.
|
||||
```powershell
|
||||
$DownloadUrl = 'https://github.com/PowerShell/PowerShellEditorServices/releases/latest/download/PowerShellEditorServices.zip';
|
||||
$ZipPath = "$HOME/Desktop/PowerShellEditorServices.zip";
|
||||
$InstallPath = "$HOME/Desktop/PowerShellEditorServices";
|
||||
Invoke-WebRequest -Method 'GET' -Uri $DownloadUrl -OutFile $ZipPath;
|
||||
Expand-Archive -Path $ZipPath -DestinationPath $InstallPath;
|
||||
```
|
||||
|
||||
### Install Neovim's Quickstart LSP Configurations
|
||||
Neovim has a repository of quickstart LSP configurations for a number of
|
||||
languages, including PowerShell. Install the quickstart LSP configuration into
|
||||
one of the package directories inside `$XDG_CONFIG_HOME`. The path
|
||||
`$XDG_CONFIG_HOME` will vary depending on which operating system you are on:
|
||||
|
||||
| OS | Path |
|
||||
| ---------- | -------------------------- |
|
||||
| Windows | `$HOME/AppData/local/nvim` |
|
||||
| *nix/macOS | `$HOME/.config/nvim` |
|
||||
|
||||
The easiest way is to install the quickstart configuration is to clone the
|
||||
repository using git:
|
||||
```powershell
|
||||
git clone https://github.com/neovim/nvim-lspconfig.git "$HOME/AppData/local/nvim/pack/complete/start/nvim-lspconfig"
|
||||
```
|
||||
|
||||
Alternatively, you can extract the zip file into the same place:
|
||||
```powershell
|
||||
$DownloadUrl = 'https://github.com/neovim/nvim-lspconfig/archive/refs/heads/master.zip';
|
||||
$ZipPath = "$HOME/AppData/local/nvim/nvim-lspconfig.zip";
|
||||
$InstallPath = "$HOME/AppData/local/nvim/pack/complete/start/nvim-lspconfig";
|
||||
Invoke-WebRequest -Method 'GET' Uri $DownloadUrl -OutFile $ZipPath;
|
||||
Expand-Archive -Path $ZipPath -DestinationPath $InstallPath;
|
||||
```
|
||||
|
||||
> NOTE: If the corresponding neovim configuration and package directories have
|
||||
> not been created yet, create them before installing the quickstart LSP
|
||||
> configuration repository.
|
||||
|
||||
### Configure the Server
|
||||
|
||||
#### Setup Keybindings and Path Information
|
||||
Once the basic language configurations have been installed, add this to your
|
||||
`init.lua` located in `$XDG_CONFIG_HOME`:
|
||||
```lua
|
||||
local on_attach = function(client, bufnr)
|
||||
-- Enable completion triggered by <c-x><c-o>
|
||||
vim.api.nvim_set_option_value("omnifunc", "v:lua.vim.lsp.omnifunc", { buf = bufnr })
|
||||
|
||||
local bufopts = { noremap = true, silent = true, buffer = bufnr }
|
||||
vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, bufopts)
|
||||
vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, bufopts)
|
||||
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, bufopts)
|
||||
vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, bufopts)
|
||||
vim.keymap.set('n', 'gr', vim.lsp.buf.references, bufopts)
|
||||
vim.keymap.set('n', 'K', vim.lsp.buf.hover, bufopts)
|
||||
vim.keymap.set('n', '<Leader>ca', vim.lsp.buf.code_action, bufopts)
|
||||
vim.keymap.set('n', '<Leader>f', function() vim.lsp.buf.format { async = true } end, bufopts)
|
||||
vim.keymap.set('n', '<Leader>rn', vim.lsp.buf.rename, bufopts)
|
||||
vim.keymap.set('n', '<Leader>td', vim.lsp.buf.type_definition, bufopts)
|
||||
end
|
||||
|
||||
local home_directory = os.getenv('HOME')
|
||||
if home_directory == nil then
|
||||
home_directory = os.getenv('USERPROFILE')
|
||||
end
|
||||
|
||||
-- The bundle_path is where PowerShell Editor Services was installed
|
||||
local bundle_path = home_directory .. '/Desktop/PowerShellEditorServices'
|
||||
|
||||
require('lspconfig')['powershell_es'].setup {
|
||||
bundle_path = bundle_path,
|
||||
on_attach = on_attach
|
||||
}
|
||||
```
|
||||
|
||||
> NOTE: Be sure to set the bundle_path variable to the correct location,
|
||||
> otherwise the server will not know the path to start the server.
|
||||
|
||||
If you use an `init.vim` file, you may put the keybinding and path configuration
|
||||
in your `init.vim` with the `lua` heredoc syntax instead.
|
||||
```vim
|
||||
lua << EOF
|
||||
-- lua keybindings and path configuration here
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Theme Troubleshooting
|
||||
If you find that your colorscheme appears correctly for a second and then
|
||||
changes to not having full highlighting, you'll need to disable semantic
|
||||
highlighting.
|
||||
Add this line to the `on_attach` function.
|
||||
```lua
|
||||
client.server_capabilities.semanticTokensProvider = nil
|
||||
```
|
||||
|
||||
#### Configure Additional Settings
|
||||
To further configure the server, you can supply settings to the setup table.
|
||||
For example, you can set the code formatting preset to one true brace style
|
||||
(OTBS).
|
||||
```lua
|
||||
require('lspconfig')['powershell_es'].setup {
|
||||
bundle_path = bundle_path,
|
||||
on_attach = on_attach,
|
||||
settings = { powershell = { codeFormatting = { Preset = 'OTBS' } } }
|
||||
}
|
||||
```
|
||||
For a more complete list of options have a look at this schema:
|
||||
[nvim-lsp-installer powershell_es reference](https://github.com/williamboman/nvim-lsp-installer/blob/main/lua/nvim-lsp-installer/_generated/schemas/powershell_es.lua)
|
||||
|
||||
You can also set the bundled PSScriptAnalyzer's custom rule path like so:
|
||||
```lua
|
||||
local custom_settings_path = home_directory .. '/PSScriptAnalyzerSettings.psd1'
|
||||
require('lspconfig')['powershell_es'].setup {
|
||||
bundle_path = bundle_path,
|
||||
on_attach = on_attach,
|
||||
settings = { powershell = { scriptAnalysis = { settingsPath = custom_settings_path } } }
|
||||
}
|
||||
```
|
||||
|
||||
#### Autocomplete Brackets Troubleshooting
|
||||
If you're using `blink.cmp` and you're getting brackets when autocompleting
|
||||
cmdlet names, you'll need to add `{ "ps1", "psm1" }` to the blocked filetypes
|
||||
for both `kind_resolution` and `semantic_token_resolution` in the plugin's
|
||||
config file.
|
||||
|
||||
[Blink.cmp completion reference](https://cmp.saghen.dev/configuration/reference#completion-accept)
|
||||
|
||||
### Indentation
|
||||
|
||||
Vim/Neovim does not contain default `:h indentexpr` for filetype `ps1`.
|
||||
So you might notice indentation on newline is not behaving as expected for powershell files.
|
||||
Luckily powershell has similar syntax like C, so we can use `:h cindent` to fix the indentation problem.
|
||||
You can use the following snippet to either callback of an autocmd or ftplugin.
|
||||
|
||||
```lua
|
||||
--- ./nvim/lua/ftplugin/ps1.lua
|
||||
|
||||
-- disable indent from powershell treesitter parser
|
||||
-- because the parse isn't mature currently
|
||||
-- you can ignore this step if don't use treesitter
|
||||
if pcall(require, 'nvim-treesitter') then
|
||||
vim.schedule(function() vim.cmd([[TSBufDisable indent]]) end)
|
||||
end
|
||||
|
||||
vim.opt_local.cindent = true
|
||||
vim.opt_local.cinoptions:append { 'J1', '(1s', '+0' } -- see :h cino-J, cino-(, cino-+
|
||||
|
||||
vim.opt_local.iskeyword:remove { '-' } -- OPTIONALLY consider Verb-Noun as a whole word
|
||||
```
|
||||
37
docs/guide/introduction.md
Normal file
37
docs/guide/introduction.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Introduction
|
||||
|
||||
> NOTE: The user guide is currently under development and may be missing
|
||||
> important information. If you feel that a particular area is missing or
|
||||
> poorly explained, please feel free to file an issue at our [GitHub site](https://github.com/PowerShell/PowerShellEditorServices/issues)
|
||||
|
||||
PowerShell Editor Services is a tool that provides useful services to code
|
||||
editors that need a great PowerShell editing experience.
|
||||
|
||||
## The .NET API
|
||||
|
||||
The .NET API provides the complete set of services which can be used in
|
||||
code editors or any other type of application. The easiest way to get
|
||||
started with it is to add the [Microsoft.PowerShell.EditorServices](https://www.nuget.org/packages/Microsoft.PowerShell.EditorServices/)
|
||||
NuGet package to your C# project.
|
||||
|
||||
If you're a developer that would like to use PowerShell Editor Services in
|
||||
a .NET application, read the page titled [Using the .NET API](using_the_dotnet_api.md)
|
||||
to learn more.
|
||||
|
||||
## The Host Process
|
||||
|
||||
The host process provides a JSON-based API wrapper around the .NET APIs so
|
||||
that editors written in non-.NET languages can make use of its capabilities.
|
||||
In the future the host process will allow the use of network-based channels
|
||||
to enable all of the APIs to be accessed remotely.
|
||||
|
||||
If you're a developer that would like to integrate PowerShell Editor Services
|
||||
into your favorite editor, read the page titled [Using the Host Process](using_the_host_process.md)
|
||||
to learn more.
|
||||
|
||||
## Writing Extensions in PowerShell
|
||||
|
||||
If you're using an editor that leverages PowerShell Editor Services to provide
|
||||
PowerShell editing capabilities, you may be able to extend its behavior using
|
||||
our PowerShell-based editor extension API. Read the page titled [Extending the
|
||||
Host Editor](extensions.md) to learn more.
|
||||
4
docs/guide/toc.md
Normal file
4
docs/guide/toc.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# [Introduction](introduction.md)
|
||||
# [Using the .NET API](using_the_dotnet_api.md)
|
||||
# [Using the Host Process](using_the_host_process.md)
|
||||
# [Extending the Host Editor](extensions.md)
|
||||
1124
docs/guide/using_the_host_process.md
Normal file
1124
docs/guide/using_the_host_process.md
Normal file
File diff suppressed because it is too large
Load diff
BIN
docs/images/PowerShell_logo.png
Normal file
BIN
docs/images/PowerShell_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
docs/images/vsc_command_palette.png
Normal file
BIN
docs/images/vsc_command_palette.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/images/vsc_editor_command_list.png
Normal file
BIN
docs/images/vsc_editor_command_list.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
27
docs/index.md
Normal file
27
docs/index.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# PowerShell Editor Services
|
||||
|
||||
PowerShell Editor Services provides common functionality that is needed
|
||||
to enable a consistent and robust PowerShell development experience
|
||||
across multiple editors.
|
||||
|
||||
## [User Guide](guide/introduction.md)
|
||||
|
||||
The User Guide describes the high level design of this project and gives
|
||||
guidance on how to use it.
|
||||
|
||||
## [API Reference](api/index.md)
|
||||
|
||||
The API Reference contains details about the .NET API.
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you run into any issues while using PowerShell Editor Services and this documentation doesn't
|
||||
answer your question, feel free to file an issue on our
|
||||
[GitHub page](https://github.com/PowerShell/PowerShellEditorServices/issues) or contact the
|
||||
via Twitter ([@daviwil](http://twitter.com/daviwil) and [@r_keith_hill](http://twitter.com/r_keith_hill))
|
||||
|
||||
## Contributing
|
||||
|
||||
We would love to incorporate community contributions into this project. If you would like to
|
||||
contribute code, documentation, tests, or bug reports, please read our [Contribution Guide]
|
||||
(../CONTRIBUTING.md) to learn more.
|
||||
55
docs/template/conceptual.html.primary.tmpl
vendored
Normal file
55
docs/template/conceptual.html.primary.tmpl
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{{!Copyright (c) Microsoft Corporation. Licensed under the MIT License.}}
|
||||
{{!include(/^styles/.*/)}}
|
||||
{{!include(/^fonts/.*/)}}
|
||||
{{!include(favicon.ico)}}
|
||||
{{!include(logo.svg)}}
|
||||
<!DOCTYPE html>
|
||||
<!--[if IE]><![endif]-->
|
||||
<html>
|
||||
{{>partials/head}}
|
||||
<body data-spy="scroll" data-target="#affix">
|
||||
<div id="wrapper">
|
||||
<header>
|
||||
{{^_disableNavbar}}
|
||||
{{>partials/navbar}}
|
||||
{{/_disableNavbar}}
|
||||
{{^_disableBreadcrumb}}
|
||||
{{>partials/breadcrumb}}
|
||||
{{/_disableBreadcrumb}}
|
||||
</header>
|
||||
<div role="main" class="container body-content">
|
||||
{{^_disableToc}}
|
||||
{{>partials/toc}}
|
||||
<div class="article row grid-right">
|
||||
{{/_disableToc}}
|
||||
{{#_disableToc}}
|
||||
<div class="article row grid">
|
||||
{{/_disableToc}}
|
||||
{{#_disableAffix}}
|
||||
<div class="col-md-12">
|
||||
{{/_disableAffix}}
|
||||
{{^_disableAffix}}
|
||||
<div class="col-md-10">
|
||||
{{/_disableAffix}}
|
||||
<article class="content wrap" id="_content">
|
||||
{{^_disableContribution}}
|
||||
{{#docurl}}
|
||||
<a href="{{docurl}}" class="pull-right mobile-hide"><span class="fa fa-github"></span> Improve this Doc</a>
|
||||
{{/docurl}}
|
||||
{{/_disableContribution}}
|
||||
{{{rawTitle}}}
|
||||
{{{conceptual}}}
|
||||
</article>
|
||||
</div>
|
||||
{{^_disableAffix}}
|
||||
{{>partials/affix}}
|
||||
{{/_disableAffix}}
|
||||
</div>
|
||||
</div>
|
||||
{{^_disableFooter}}
|
||||
{{>partials/footer}}
|
||||
{{/_disableFooter}}
|
||||
</div>
|
||||
{{>partials/scripts}}
|
||||
</body>
|
||||
</html>
|
||||
135
docs/template/partials/class.tmpl.partial
vendored
Normal file
135
docs/template/partials/class.tmpl.partial
vendored
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
{{!Copyright (c) Microsoft Corporation. Licensed under the MIT License.}}
|
||||
|
||||
{{^_disableContribution}}
|
||||
{{#docurl}}<a href="{{docurl}}" class="pull-right mobile-hide"><span class="fa fa-github"></span>{{__global.improveThisDoc}}</a>{{/docurl}}
|
||||
{{#sourceurl}}<a href="{{sourceurl}}" class="pull-right mobile-hide">{{__global.viewSource}}</a>{{/sourceurl}}
|
||||
{{/_disableContribution}}
|
||||
<h1 id="{{id}}" data-uid="{{uid}}">{{>partials/title}}</h1>
|
||||
<div class="markdown level0 summary">{{{summary}}}</div>
|
||||
<div class="markdown level0 conceptual">{{{conceptual}}}</div>
|
||||
{{#inheritance.0}}
|
||||
<div class="inheritance">
|
||||
<h5>{{__global.inheritance}}</h5>
|
||||
{{#inheritance}}
|
||||
<div class="level{{index}}">{{{specName.0.value}}}</div>
|
||||
{{/inheritance}}
|
||||
<div class="level{{item.level}}"><span class="xref">{{item.name.0.value}}</span></div>
|
||||
</div>
|
||||
{{/inheritance.0}}
|
||||
<h6><strong>{{__global.namespace}}</strong>:{{namespace}}</h6>
|
||||
<h6><strong>{{__global.assembly}}</strong>:{{assemblies.0}}.dll</h6>
|
||||
<h5 id="{{id}}_syntax">{{__global.syntax}}</h5>
|
||||
<div class="codewrapper">
|
||||
<pre><code class="lang-{{_lang}} hljs">{{syntax.content.0.value}}</code></pre>
|
||||
</div>
|
||||
{{#remarks}}
|
||||
<h5 id="{{id}}_remarks"><strong>{{__global.remarks}}</strong></h5>
|
||||
<div class="markdown level0 remarks">{{{remarks}}}</div>
|
||||
{{/remarks}}
|
||||
{{#children}}
|
||||
<h3 id="{{id}}">{{>partials/classSubtitle}}</h3>
|
||||
{{#children}}
|
||||
{{^_disableContribution}}
|
||||
{{#docurl}}
|
||||
<span class="small pull-right mobile-hide">
|
||||
<span class="divider">|</span>
|
||||
<a href="{{docurl}}">{{__global.improveThisDoc}}</a>
|
||||
</span>{{/docurl}}
|
||||
{{#sourceurl}}
|
||||
<span class="small pull-right mobile-hide">
|
||||
<a href="{{sourceurl}}">{{__global.viewSource}}</a>
|
||||
</span>{{/sourceurl}}
|
||||
{{/_disableContribution}}
|
||||
<h4 id="{{id}}" data-uid="{{uid}}">{{name.0.value}}</h4>
|
||||
<div class="markdown level1 summary">{{{summary}}}</div>
|
||||
<div class="markdown level1 conceptual">{{{conceptual}}}</div>
|
||||
{{#remarks}}
|
||||
<h5 id="{{id}}_remarks">{{__global.remarks}}</h5>
|
||||
<div class="markdown level1 remarks">{{{remarks}}}</div>
|
||||
{{/remarks}}
|
||||
<h5 class="decalaration">{{__global.declaration}}</h5>
|
||||
{{#syntax}}
|
||||
<div class="codewrapper">
|
||||
<pre><code class="lang-{{_lang}} hljs">{{syntax.content.0.value}}</code></pre>
|
||||
</div>
|
||||
{{#parameters.0}}
|
||||
<h5 class="parameters">{{__global.parameters}}</h5>
|
||||
<table class="table table-bordered table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{__global.type}}</th>
|
||||
<th>{{__global.name}}</th>
|
||||
<th>{{__global.description}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{/parameters.0}}
|
||||
{{#parameters}}
|
||||
<tr>
|
||||
<td>{{{type.specName.0.value}}}</td>
|
||||
<td><em>{{{id}}}</em></td>
|
||||
<td>{{{description}}}</td>
|
||||
</tr>
|
||||
{{/parameters}}
|
||||
{{#parameters.0}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/parameters.0}}
|
||||
{{#return}}
|
||||
<h5 class="returns">{{__global.returns}}</h5>
|
||||
<table class="table table-bordered table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{__global.type}}</th>
|
||||
<th>{{__global.description}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{{type.specName.0.value}}}</td>
|
||||
<td>{{{description}}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/return}}
|
||||
{{#propertyValue}}
|
||||
<h5 class="propertyValue">{{__global.provertyValue}}</h5>
|
||||
<table class="table table-bordered table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{__global.type}}</th>
|
||||
<th>{{__global.description}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{{type.specName.0.value}}}</td>
|
||||
<td>{{{description}}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/propertyValue}}
|
||||
{{/syntax}}
|
||||
{{#exceptions.0}}
|
||||
<h5 class="exceptions">{{__global.exceptions}}</h5>
|
||||
<table class="table table-bordered table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{__global.type}}</th>
|
||||
<th>{{__global.condition}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{/exceptions.0}}
|
||||
{{#exceptions}}
|
||||
<tr>
|
||||
<td>{{{type.specName.0.value}}}</td>
|
||||
<td>{{{description}}}</td>
|
||||
</tr>
|
||||
{{/exceptions}}
|
||||
{{#exceptions.0}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/exceptions.0}}
|
||||
{{/children}}
|
||||
{{/children}}
|
||||
7
docs/template/partials/footer.tmpl.partial
vendored
Normal file
7
docs/template/partials/footer.tmpl.partial
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{{!Copyright (c) Microsoft Corporation. Licensed under the MIT License.}}
|
||||
|
||||
<footer>
|
||||
<div class="inner-footer">
|
||||
<p>© Microsoft // Generated with <a href="https://github.com/dotnet/docfx">DocFX</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
28
docs/template/partials/head.tmpl.partial
vendored
Normal file
28
docs/template/partials/head.tmpl.partial
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{{!Copyright (c) Microsoft Corporation. Licensed under the MIT License.}}
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="title" content="{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}}">
|
||||
{{#_description}}<meta name="description" content="{{_description}}">{{/_description}}
|
||||
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.vendor.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/main.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/docfx.css">
|
||||
<link rel="stylesheet" href="{{_rel}}styles/style.css">
|
||||
<meta property="docfx:navrel" content="{{_navRel}}">
|
||||
<meta property="docfx:tocrel" content="{{_tocRel}}">
|
||||
<!--
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-63735192-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
-->
|
||||
</head>
|
||||
21
docs/template/partials/namespace.tmpl.partial
vendored
Normal file
21
docs/template/partials/namespace.tmpl.partial
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{{!Copyright (c) Microsoft Corporation. Licensed under the MIT License.}}
|
||||
|
||||
{{^_disableContribution}}
|
||||
{{#docurl}}
|
||||
<a href="{{docurl}}" class="pull-right mobile-hide"><span class="fa fa-github"></span>{{__global.improveThisDoc}}</a>
|
||||
{{/docurl}}
|
||||
{{#sourceurl}}
|
||||
<a href="{{sourceurl}}" class="pull-right mobile-hide">{{__global.viewSource}}</a>
|
||||
{{/sourceurl}}
|
||||
{{/_disableContribution}}
|
||||
<h1 id="{{id}}" data-uid="{{uid}}">{{>partials/title}}</h1>
|
||||
<div class="markdown level0 summary">{{{summary}}}</div>
|
||||
<div class="markdown level0 conceptual">{{{conceptual}}}</div>
|
||||
<div class="markdown level0 remarks">{{{remarks}}}</div>
|
||||
{{#children}}
|
||||
<h3 id="{{id}}">{{>partials/namespaceSubtitle}}</h3>
|
||||
{{#children}}
|
||||
<h4>{{{specName.0.value}}}</h4>
|
||||
<section>{{{summary}}}</section>
|
||||
{{/children}}
|
||||
{{/children}}
|
||||
18
docs/template/partials/navbar.tmpl.partial
vendored
Normal file
18
docs/template/partials/navbar.tmpl.partial
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{{!Copyright (c) Microsoft Corporation. Licensed under the MIT License.}}
|
||||
|
||||
<nav class="navbar navbar-inverse">
|
||||
<div class="container">
|
||||
<a href="{{_rel}}"><img height="50px" src="{{_rel}}images/PowerShell_logo.png" style="float:right;"/></a>
|
||||
<div class="navbar-header ">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="{{_rel}}"><span class="dotnet">PowerShell Editor Services</span></a>
|
||||
</div>
|
||||
<div id="navbar" class="collapse navbar-collapse">
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
</nav>
|
||||
268
docs/template/styles/main.css
vendored
Normal file
268
docs/template/styles/main.css
vendored
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
@import url(//fonts.googleapis.com/css?family=Roboto+Condensed:700);
|
||||
@import url(//fonts.googleapis.com/css?family=Open+Sans);
|
||||
|
||||
/* Main styles */
|
||||
body {
|
||||
font-family: "Open Sans", "Segoe UI", sans-serif;
|
||||
font-size: 15px;
|
||||
padding-top: 50px;
|
||||
}
|
||||
ul {
|
||||
list-style-image: url("../../images/core/list-bullet.png");
|
||||
}
|
||||
nav {
|
||||
font-size: 14px;
|
||||
}
|
||||
.navbar-nav > li > a.nav-active, .navbar-nav > li > a.nav-active:hover {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
font-family: "Roboto Condensed", "Segoe UI", sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
color: #c0c0c0;
|
||||
}
|
||||
footer > .inner-footer a {
|
||||
color: #c0c0c0;
|
||||
text-decoration: none;
|
||||
}
|
||||
footer > .inner-footer a:hover {
|
||||
color: #32145a;
|
||||
text-decoration: none;
|
||||
}
|
||||
.content a {
|
||||
/*color: #A979B3;*/
|
||||
color: #A356B3;
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
}
|
||||
.content a:hover {
|
||||
/*transition: color .15s cubic-bezier(.33, .66, .66, 1);*/
|
||||
text-decoration: none;
|
||||
color: #682079;
|
||||
}
|
||||
|
||||
|
||||
/* End of main styles */
|
||||
|
||||
/* Index page styles */
|
||||
.btn-hero-core {
|
||||
padding: 15px 25px;
|
||||
background-color: #32145a;
|
||||
color: #d89ae4;
|
||||
display: inline-block;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-left: 20px;
|
||||
-webkit-box-shadow: 2px 2px 3px 0px #2C0D33; /* Safari 3-4, iOS 4.0.2 - 4.2, Android 2.3+ */
|
||||
-moz-box-shadow: 2px 2px 3px 0px #2C0D33; /* Firefox 3.5 - 3.6 */
|
||||
box-shadow: 2px 2px 3px 0px #2C0D33; /* Opera 10.5, IE 9, Firefox 4+, Chrome 6+, iOS 5 */
|
||||
}
|
||||
.btn-hero-core:hover {
|
||||
color: #d89ae4;
|
||||
text-decoration: none;
|
||||
}
|
||||
.hero {
|
||||
background-color: #682079;
|
||||
width: inherit;
|
||||
color: #fff;
|
||||
}
|
||||
.starter-template {
|
||||
padding: 40px 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.dotnet {
|
||||
color: #fff;
|
||||
}
|
||||
#rest-vps {
|
||||
display: none;
|
||||
}
|
||||
.value-prop-heading {
|
||||
margin-top: 0px;
|
||||
}
|
||||
.value-props {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.intro-image {
|
||||
text-align: center;
|
||||
}
|
||||
.intro-image > img {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* End of index page styles */
|
||||
|
||||
/* Getting started page styles */
|
||||
.getting-started-intro {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.getting-started-intro > h2, h4 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.btn-gs {
|
||||
width: 150px;
|
||||
}
|
||||
.btn-gs:hover, .btn-gs:active, .btn-gs:focus, .jquery-active {
|
||||
color: #fff;
|
||||
background-color: #682079;
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.step {
|
||||
width: 100%;
|
||||
margin: 50px auto;
|
||||
padding: 20px 0px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border: solid 1px #c0c0c0;
|
||||
min-height: 300px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.step-block {
|
||||
display: block;
|
||||
}
|
||||
.step-none {
|
||||
display: none;
|
||||
}
|
||||
.step-number {
|
||||
position: relative;
|
||||
top: -40px;
|
||||
background-color: #32145a;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
z-index: 999;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 80px;
|
||||
padding: 10px;
|
||||
border: solid 1px #c0c0c0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.step > h3 {
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
font-size: 30px;
|
||||
}
|
||||
.step > p {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
width: 70%;
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.code-sample {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
|
||||
/* Terminal backgrounds */
|
||||
.terminal {
|
||||
display: block;
|
||||
width: 850px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.terminal-titlebar {
|
||||
background-color: #c0c0c0;
|
||||
height: 30px;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
font-family: "Consolas", "Monaco", monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
word-wrap: break-word;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
.prompt {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
color: #c0c0c0;
|
||||
}
|
||||
.windows-prompt:after {
|
||||
content: 'PS > ';
|
||||
}
|
||||
.unix-prompt:after {
|
||||
content: '~$ ';
|
||||
}
|
||||
|
||||
@media (max-device-width: 480px) and (orientation: portrait), (max-device-width: 700px) and (orientation: landscape){
|
||||
/* Index page overrides */
|
||||
.btn-hero-core {
|
||||
padding: 10px 15px;
|
||||
margin-left: 0px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.intro-image > img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Overview overrides */
|
||||
img[src*="10kft_view"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Getting started overrides */
|
||||
.btn-gs {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn-gs:hover, .btn-gs:active, .btn-gs:focus, .jquery-active {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.step {
|
||||
width: 90%;
|
||||
font-size: 14px;
|
||||
}
|
||||
.step > h3 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.step-number {
|
||||
width: 40px;
|
||||
font-size: 18px;
|
||||
padding: 5px;
|
||||
}
|
||||
.terminal {
|
||||
width: 95%;
|
||||
}
|
||||
.terminal-titlebar {
|
||||
height: 20px;
|
||||
}
|
||||
.terminal-body {
|
||||
font-size: 12px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
43
docs/template/styles/style.css
vendored
Normal file
43
docs/template/styles/style.css
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
body {
|
||||
font-family: "Open Sans", "Segoe UI", sans-serif;
|
||||
padding-top: 0px;
|
||||
}
|
||||
footer {
|
||||
z-index: 0;
|
||||
}
|
||||
.navbar-brand {
|
||||
font-size: 18px;
|
||||
padding: 15px;
|
||||
}
|
||||
.toc .level3 {
|
||||
font-weight: normal;
|
||||
margin-top: 5px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
a.pull-right {
|
||||
margin-left: 10px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
article.content > h1 {
|
||||
word-break: break-word;
|
||||
}
|
||||
@media only screen and (max-width: 768px) {
|
||||
.toc .level3 > li {
|
||||
display: inline-block;
|
||||
}
|
||||
.toc .level3 > li:after {
|
||||
margin-left: -3px;
|
||||
margin-right: 5px;
|
||||
content: ", ";
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
@media (max-width: 260px) {
|
||||
.toc .level3 > li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toc .level3 > li:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
5
docs/toc.yml
Normal file
5
docs/toc.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
- name: User Guide
|
||||
href: guide/
|
||||
homepage: guide/introduction.md
|
||||
- name: API Reference
|
||||
href: api/
|
||||
80
flake.nix
Normal file
80
flake.nix
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
description = "PowerShellEditorServices (PSES)";
|
||||
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
|
||||
psreadline = pkgs.fetchzip {
|
||||
url = "https://www.powershellgallery.com/api/v2/package/PSReadLine/2.4.5";
|
||||
sha256 = "12pwy5b426lv7cmlasa7ajy202a0vjjs9drfyg2q5zwqxhj8g8cz";
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
psscriptanalyzer = pkgs.fetchzip {
|
||||
url = "https://www.powershellgallery.com/api/v2/package/PSScriptAnalyzer/1.24.0";
|
||||
sha256 = "03bz9f5rcx749758h6l9wsv86300s0a34qgd522v6166crbpvg2p";
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
invokebuild = pkgs.fetchzip {
|
||||
url = "https://www.powershellgallery.com/api/v2/package/InvokeBuild/5.12.2";
|
||||
sha256 = "0cry70c6ndlv4y0fmlg5iip0ix1r51p7i6dxx3y2zzczbkz2mn6m";
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
platyps = pkgs.fetchzip {
|
||||
url = "https://www.powershellgallery.com/api/v2/package/platyPS/0.14.2";
|
||||
sha256 = "13m11mf42blcn2327m9a4dqfg8sgjjg1vg9dmiwvnbq7wpam260q";
|
||||
stripRoot = false;
|
||||
};
|
||||
in
|
||||
{
|
||||
packages.${system}.pses = pkgs.stdenv.mkDerivation {
|
||||
pname = "powershell-editor-services";
|
||||
version = "4.4.0";
|
||||
src = ./.;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.dotnet-sdk_8
|
||||
pkgs.git
|
||||
pkgs.powershell
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
export HOME=$TMPDIR
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
export NUGET_PACKAGES=$TMPDIR/nuget
|
||||
|
||||
git init -q
|
||||
git config user.email "nix@local"
|
||||
git config user.name "nix"
|
||||
git add -A
|
||||
git commit -qm "nix build"
|
||||
|
||||
mkdir -p module
|
||||
cp -r ${psreadline} module/PSReadLine
|
||||
cp -r ${psscriptanalyzer} module/PSScriptAnalyzer
|
||||
|
||||
mkdir -p $TMPDIR/psmodules
|
||||
cp -r ${invokebuild} $TMPDIR/psmodules/InvokeBuild
|
||||
cp -r ${platyps} $TMPDIR/psmodules/platyPS
|
||||
export PSModulePath="$TMPDIR/psmodules${"PSModulePath:+:$PSModulePath"}"
|
||||
|
||||
pwsh -NoLogo -NoProfile -Command \
|
||||
"Import-Module InvokeBuild; Invoke-Build -File PowerShellEditorServices.build.ps1 -Task AssembleModule -Configuration Release"
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/share/powershell/Modules
|
||||
cp -r module/* $out/share/powershell/Modules/
|
||||
'';
|
||||
};
|
||||
|
||||
packages.${system}.default = self.packages.${system}.pses;
|
||||
};
|
||||
}
|
||||
7
global.json
Normal file
7
global.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "8.0.416",
|
||||
"rollForward": "latestFeature",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
}
|
||||
7
nuget.config
Normal file
7
nuget.config
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="PowerShellCore_PublicPackages" value="https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/powershell/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
15
src/PowerShellEditorServices.Hosting/BuildInfo.cs
Normal file
15
src/PowerShellEditorServices.Hosting/BuildInfo.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
public static class BuildInfo
|
||||
{
|
||||
public static readonly string BuildVersion = "<development-build>";
|
||||
public static readonly string BuildOrigin = "<development>";
|
||||
public static readonly string BuildCommit = "<development>";
|
||||
public static readonly System.DateTime? BuildTime = System.DateTime.Parse("2019-12-06T21:43:41", CultureInfo.InvariantCulture.DateTimeFormat);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,517 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.PowerShell.Commands;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Management.Automation;
|
||||
using System.Reflection;
|
||||
using SMA = System.Management.Automation;
|
||||
using System.Management.Automation.Runspaces;
|
||||
using Microsoft.PowerShell.EditorServices.Hosting;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
#if DEBUG
|
||||
using System.Threading;
|
||||
using Debugger = System.Diagnostics.Debugger;
|
||||
#endif
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services.
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsLifecycle.Start, "EditorServices", DefaultParameterSetName = "NamedPipe")]
|
||||
public sealed class StartEditorServicesCommand : PSCmdlet
|
||||
{
|
||||
private readonly List<IDisposable> _disposableResources;
|
||||
|
||||
private readonly List<IDisposable> _loggerUnsubscribers;
|
||||
|
||||
private HostLogger _logger;
|
||||
|
||||
// NOTE: Ignore the suggestion to use Environment.ProcessId as it doesn't work for
|
||||
// .NET 4.6.2 (for Windows PowerShell), and this won't be caught in CI.
|
||||
private static readonly int s_currentPID = Process.GetCurrentProcess().Id;
|
||||
|
||||
public StartEditorServicesCommand()
|
||||
{
|
||||
// Sets the distribution channel to "PSES" so starts can be distinguished in PS7+ telemetry
|
||||
Environment.SetEnvironmentVariable("POWERSHELL_DISTRIBUTION_CHANNEL", "PSES");
|
||||
_disposableResources = new List<IDisposable>();
|
||||
_loggerUnsubscribers = new List<IDisposable>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the EditorServices host to report.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string HostName { get; set; } = "PSES";
|
||||
|
||||
/// <summary>
|
||||
/// The ID to give to the host's profile.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string HostProfileId { get; set; } = "PSES";
|
||||
|
||||
/// <summary>
|
||||
/// The version to report for the host.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public Version HostVersion { get; set; } = new Version(0, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Path to the session file to create on startup or startup failure.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string SessionDetailsPath { get; set; } = "PowerShellEditorServices.json";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the named pipe to use for the LSP transport.
|
||||
/// If left unset and named pipes are used as transport, a new name will be generated.
|
||||
/// </summary>
|
||||
[Parameter(ParameterSetName = "NamedPipe")]
|
||||
public string LanguageServicePipeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the named pipe to use for the debug adapter transport.
|
||||
/// If left unset and named pipes are used as a transport, a new name will be generated.
|
||||
/// </summary>
|
||||
[Parameter(ParameterSetName = "NamedPipe")]
|
||||
public string DebugServicePipeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the input named pipe to use for the LSP transport.
|
||||
/// </summary>
|
||||
[Parameter(ParameterSetName = "NamedPipeSimplex")]
|
||||
public string LanguageServiceInPipeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the output named pipe to use for the LSP transport.
|
||||
/// </summary>
|
||||
[Parameter(ParameterSetName = "NamedPipeSimplex")]
|
||||
public string LanguageServiceOutPipeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the input pipe to use for the debug adapter transport.
|
||||
/// </summary>
|
||||
[Parameter(ParameterSetName = "NamedPipeSimplex")]
|
||||
public string DebugServiceInPipeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the output pipe to use for the debug adapter transport.
|
||||
/// </summary>
|
||||
[Parameter(ParameterSetName = "NamedPipeSimplex")]
|
||||
public string DebugServiceOutPipeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, uses standard input/output as the LSP transport.
|
||||
/// When <see cref="DebugServiceOnly"/> is set with this, standard input/output
|
||||
/// is used as the debug adapter transport.
|
||||
/// </summary>
|
||||
[Parameter(ParameterSetName = "Stdio")]
|
||||
public SwitchParameter Stdio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to where PowerShellEditorServices and its bundled modules are.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string BundledModulesPath { get; set; } = Path.GetFullPath(Path.Combine(
|
||||
Path.GetDirectoryName(typeof(StartEditorServicesCommand).Assembly.Location),
|
||||
"..", "..", ".."));
|
||||
|
||||
/// <summary>
|
||||
/// The absolute path to the folder where logs will be saved.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string LogPath { get; set; } = Path.Combine(Path.GetTempPath(), "PowerShellEditorServices");
|
||||
|
||||
/// <summary>
|
||||
/// The minimum log level that should be emitted.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string LogLevel { get; set; } = PsesLogLevel.Warning.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Paths to additional PowerShell modules to be imported at startup.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string[] AdditionalModules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Any feature flags to enable in EditorServices.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string[] FeatureFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When set, enables the Extension Terminal.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public SwitchParameter EnableConsoleRepl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When set and the console is enabled, the legacy lightweight
|
||||
/// readline implementation will be used instead of PSReadLine.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public SwitchParameter UseLegacyReadLine { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When set, do not enable LSP service, only the debug adapter.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public SwitchParameter DebugServiceOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When set, do not enable debug adapter, only the language service.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public SwitchParameter LanguageServiceOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When set with a debug build, startup will wait for a debugger to attach.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public SwitchParameter WaitForDebugger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When set, will generate two simplex named pipes using a single named pipe name.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public SwitchParameter SplitInOutPipes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The banner/logo to display when the extension terminal is first started.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string StartupBanner { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compatibility to store the currently supported PSESLogLevel Enum Value
|
||||
/// </summary>
|
||||
private PsesLogLevel _psesLogLevel = PsesLogLevel.Warning;
|
||||
|
||||
#pragma warning disable IDE0022
|
||||
protected override void BeginProcessing()
|
||||
{
|
||||
#if DEBUG
|
||||
if (WaitForDebugger)
|
||||
{
|
||||
// NOTE: Ignore the suggestion to use Environment.ProcessId as it doesn't work for
|
||||
// .NET 4.6.2 (for Windows PowerShell), and this won't be caught in CI.
|
||||
Console.WriteLine($"Waiting for debugger to attach, PID: {s_currentPID}");
|
||||
while (!Debugger.IsAttached)
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Set up logging now for use throughout startup
|
||||
StartLogging();
|
||||
}
|
||||
#pragma warning restore IDE0022
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "We have to wait here, it's the whole program.")]
|
||||
protected override void EndProcessing()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Beginning EndProcessing block");
|
||||
try
|
||||
{
|
||||
// First try to remove PSReadLine to decomplicate startup
|
||||
// If PSReadLine is enabled, it will be re-imported later
|
||||
RemovePSReadLineForStartup();
|
||||
|
||||
// Create the configuration from parameters
|
||||
EditorServicesConfig editorServicesConfig = CreateConfigObject();
|
||||
|
||||
using EditorServicesLoader psesLoader = EditorServicesLoader.Create(_logger, editorServicesConfig, SessionDetailsPath, _loggerUnsubscribers);
|
||||
_logger.Log(PsesLogLevel.Debug, "Loading EditorServices");
|
||||
// Synchronously start editor services and wait here until it shuts down.
|
||||
psesLoader.LoadAndRunEditorServicesAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogException("Exception encountered starting EditorServices", e);
|
||||
|
||||
// Give the user a chance to read the message if they have a console
|
||||
if (!Stdio)
|
||||
{
|
||||
Host.UI.WriteLine("\n== Press any key to close terminal ==");
|
||||
Console.ReadKey();
|
||||
}
|
||||
|
||||
ThrowTerminatingError(new ErrorRecord(e, "PowerShellEditorServicesError", ErrorCategory.NotSpecified, this));
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (IDisposable disposableResource in _disposableResources)
|
||||
{
|
||||
disposableResource.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartLogging()
|
||||
{
|
||||
bool isLegacyPsesLogLevel = false;
|
||||
if (!Enum.TryParse(LogLevel, true, out _psesLogLevel))
|
||||
{
|
||||
// PSES used to have log levels that didn't match MEL levels, this is an adapter for those types and may eventually be removed once people migrate their settings.
|
||||
isLegacyPsesLogLevel = true;
|
||||
_psesLogLevel = LogLevel switch
|
||||
{
|
||||
"Diagnostic" => PsesLogLevel.Trace,
|
||||
"Verbose" => PsesLogLevel.Debug,
|
||||
"Normal" => PsesLogLevel.Information,
|
||||
_ => PsesLogLevel.Trace
|
||||
};
|
||||
}
|
||||
|
||||
_logger = new HostLogger(_psesLogLevel);
|
||||
if (isLegacyPsesLogLevel)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Warning, $"The log level '{LogLevel}' is deprecated and will be removed in a future release. Please update your settings or command line options to use one of the following options: 'Trace', 'Debug', 'Information', 'Warning', 'Error', 'Critical'.");
|
||||
}
|
||||
|
||||
// We need to not write log messages to Stdio
|
||||
// if it's being used as a protocol transport
|
||||
if (!Stdio)
|
||||
{
|
||||
PSHostLogger hostLogger = new(Host.UI);
|
||||
_loggerUnsubscribers.Add(_logger.Subscribe(hostLogger));
|
||||
}
|
||||
|
||||
string logDirPath = GetLogDirPath();
|
||||
string logPath = Path.Combine(logDirPath, $"StartEditorServices-{s_currentPID}.log");
|
||||
|
||||
if (File.Exists(logPath))
|
||||
{
|
||||
int randomInt = new Random().Next();
|
||||
logPath = Path.Combine(logDirPath, $"StartEditorServices-{s_currentPID}-{randomInt.ToString("X", CultureInfo.InvariantCulture.NumberFormat)}.log");
|
||||
}
|
||||
|
||||
StreamLogger fileLogger = StreamLogger.CreateWithNewFile(logPath);
|
||||
_disposableResources.Add(fileLogger);
|
||||
IDisposable fileLoggerUnsubscriber = _logger.Subscribe(fileLogger);
|
||||
fileLogger.AddUnsubscriber(fileLoggerUnsubscriber);
|
||||
_loggerUnsubscribers.Add(fileLoggerUnsubscriber);
|
||||
_logger.Log(PsesLogLevel.Trace, "Logging started");
|
||||
}
|
||||
|
||||
// Sanitizes user input and ensures the directory is created.
|
||||
private string GetLogDirPath()
|
||||
{
|
||||
string logDir = LogPath;
|
||||
if (string.IsNullOrEmpty(logDir))
|
||||
{
|
||||
logDir = Path.Combine(Path.GetTempPath(), "PowerShellEditorServices");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(logDir);
|
||||
return logDir;
|
||||
}
|
||||
|
||||
private void RemovePSReadLineForStartup()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Debug, "Removing PSReadLine");
|
||||
using SMA.PowerShell pwsh = SMA.PowerShell.Create(RunspaceMode.CurrentRunspace);
|
||||
bool hasPSReadLine = pwsh.AddCommand(new CmdletInfo(@"Microsoft.PowerShell.Core\Get-Module", typeof(GetModuleCommand)))
|
||||
.AddParameter("Name", "PSReadLine")
|
||||
.Invoke()
|
||||
.Count > 0;
|
||||
|
||||
if (hasPSReadLine)
|
||||
{
|
||||
pwsh.Commands.Clear();
|
||||
|
||||
pwsh.AddCommand(new CmdletInfo(@"Microsoft.PowerShell.Core\Remove-Module", typeof(RemoveModuleCommand)))
|
||||
.AddParameter("Name", "PSReadLine")
|
||||
.AddParameter("ErrorAction", "SilentlyContinue");
|
||||
|
||||
_logger.Log(PsesLogLevel.Debug, "Removed PSReadLine");
|
||||
}
|
||||
}
|
||||
|
||||
private EditorServicesConfig CreateConfigObject()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Creating host configuration");
|
||||
|
||||
string bundledModulesPath = BundledModulesPath;
|
||||
if (!Path.IsPathRooted(bundledModulesPath))
|
||||
{
|
||||
// For compatibility, the bundled modules path is relative to the PSES bin directory
|
||||
// Ideally it should be one level up, the PSES module root
|
||||
bundledModulesPath = Path.GetFullPath(
|
||||
Path.Combine(
|
||||
Assembly.GetExecutingAssembly().Location,
|
||||
"..",
|
||||
bundledModulesPath));
|
||||
}
|
||||
|
||||
PSObject profile = (PSObject)GetVariableValue("profile");
|
||||
|
||||
HostInfo hostInfo = new(HostName, HostProfileId, HostVersion);
|
||||
|
||||
InitialSessionState initialSessionState = Runspace.DefaultRunspace.InitialSessionState;
|
||||
initialSessionState.LanguageMode = Runspace.DefaultRunspace.SessionStateProxy.LanguageMode;
|
||||
|
||||
EditorServicesConfig editorServicesConfig = new(
|
||||
hostInfo,
|
||||
Host,
|
||||
SessionDetailsPath,
|
||||
bundledModulesPath,
|
||||
LogPath)
|
||||
{
|
||||
FeatureFlags = FeatureFlags,
|
||||
LogLevel = _psesLogLevel,
|
||||
ConsoleRepl = GetReplKind(),
|
||||
UseNullPSHostUI = Stdio, // If Stdio is used we can't write anything else out
|
||||
AdditionalModules = AdditionalModules,
|
||||
LanguageServiceTransport = GetLanguageServiceTransport(),
|
||||
DebugServiceTransport = GetDebugServiceTransport(),
|
||||
InitialSessionState = initialSessionState,
|
||||
ProfilePaths = new ProfilePathConfig
|
||||
{
|
||||
AllUsersAllHosts = GetProfilePathFromProfileObject(profile, ProfileUserKind.AllUsers, ProfileHostKind.AllHosts),
|
||||
AllUsersCurrentHost = GetProfilePathFromProfileObject(profile, ProfileUserKind.AllUsers, ProfileHostKind.CurrentHost),
|
||||
CurrentUserAllHosts = GetProfilePathFromProfileObject(profile, ProfileUserKind.CurrentUser, ProfileHostKind.AllHosts),
|
||||
CurrentUserCurrentHost = GetProfilePathFromProfileObject(profile, ProfileUserKind.CurrentUser, ProfileHostKind.CurrentHost),
|
||||
},
|
||||
};
|
||||
|
||||
if (StartupBanner != null)
|
||||
{
|
||||
editorServicesConfig.StartupBanner = StartupBanner;
|
||||
}
|
||||
|
||||
return editorServicesConfig;
|
||||
}
|
||||
|
||||
private string GetProfilePathFromProfileObject(PSObject profileObject, ProfileUserKind userKind, ProfileHostKind hostKind)
|
||||
{
|
||||
string profilePathName = $"{userKind}{hostKind}";
|
||||
if (profileObject is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string pwshProfilePath = (string)profileObject.Properties[profilePathName].Value;
|
||||
|
||||
if (hostKind == ProfileHostKind.AllHosts)
|
||||
{
|
||||
return pwshProfilePath;
|
||||
}
|
||||
|
||||
return Path.Combine(
|
||||
Path.GetDirectoryName(pwshProfilePath),
|
||||
$"{HostProfileId}_profile.ps1");
|
||||
}
|
||||
|
||||
// We should only use PSReadLine if we specified that we want a console repl
|
||||
// and we have not explicitly said to use the legacy ReadLine.
|
||||
// We also want it if we are either:
|
||||
// * On Windows on any version OR
|
||||
// * On Linux or macOS on any version greater than or equal to 7
|
||||
private ConsoleReplKind GetReplKind()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Determining REPL kind");
|
||||
|
||||
if (Stdio || !EnableConsoleRepl)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "REPL configured as None");
|
||||
return ConsoleReplKind.None;
|
||||
}
|
||||
|
||||
if (UseLegacyReadLine)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "REPL configured as Legacy");
|
||||
return ConsoleReplKind.LegacyReadLine;
|
||||
}
|
||||
|
||||
_logger.Log(PsesLogLevel.Trace, "REPL configured as PSReadLine");
|
||||
return ConsoleReplKind.PSReadLine;
|
||||
}
|
||||
|
||||
private ITransportConfig GetLanguageServiceTransport()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Configuring LSP transport");
|
||||
|
||||
if (DebugServiceOnly)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "No LSP transport: PSES is debug only");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Stdio)
|
||||
{
|
||||
return new StdioTransportConfig(_logger);
|
||||
}
|
||||
|
||||
if (LanguageServiceInPipeName != null && LanguageServiceOutPipeName != null)
|
||||
{
|
||||
return SimplexNamedPipeTransportConfig.Create(_logger, LanguageServiceInPipeName, LanguageServiceOutPipeName);
|
||||
}
|
||||
|
||||
if (SplitInOutPipes)
|
||||
{
|
||||
return SimplexNamedPipeTransportConfig.Create(_logger, LanguageServicePipeName);
|
||||
}
|
||||
|
||||
return DuplexNamedPipeTransportConfig.Create(_logger, LanguageServicePipeName);
|
||||
}
|
||||
|
||||
private ITransportConfig GetDebugServiceTransport()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Configuring debug transport");
|
||||
|
||||
if (LanguageServiceOnly)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "No Debug transport: PSES is language service only");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Stdio)
|
||||
{
|
||||
if (DebugServiceOnly)
|
||||
{
|
||||
return new StdioTransportConfig(_logger);
|
||||
}
|
||||
|
||||
_logger.Log(PsesLogLevel.Trace, "No debug transport: Transport is Stdio with debug disabled");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DebugServiceInPipeName != null && DebugServiceOutPipeName != null)
|
||||
{
|
||||
return SimplexNamedPipeTransportConfig.Create(_logger, DebugServiceInPipeName, DebugServiceOutPipeName);
|
||||
}
|
||||
|
||||
if (SplitInOutPipes)
|
||||
{
|
||||
return SimplexNamedPipeTransportConfig.Create(_logger, DebugServicePipeName);
|
||||
}
|
||||
|
||||
return DuplexNamedPipeTransportConfig.Create(_logger, DebugServicePipeName);
|
||||
}
|
||||
|
||||
private enum ProfileHostKind
|
||||
{
|
||||
AllHosts,
|
||||
CurrentHost,
|
||||
}
|
||||
|
||||
private enum ProfileUserKind
|
||||
{
|
||||
AllUsers,
|
||||
CurrentUser,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Management.Automation.Host;
|
||||
using System.Management.Automation.Runspaces;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the desired console REPL for the Extension Terminal.
|
||||
/// </summary>
|
||||
public enum ConsoleReplKind
|
||||
{
|
||||
/// <summary>No console REPL - there will be no interactive console available.</summary>
|
||||
None = 0,
|
||||
/// <summary>Use a REPL with the legacy readline implementation. This is generally used when PSReadLine is unavailable.</summary>
|
||||
LegacyReadLine = 1,
|
||||
/// <summary>Use a REPL with the PSReadLine module for console interaction.</summary>
|
||||
PSReadLine = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for editor services startup.
|
||||
/// </summary>
|
||||
public sealed class EditorServicesConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new editor services config object,
|
||||
/// with all required fields.
|
||||
/// </summary>
|
||||
/// <param name="hostInfo">The host description object.</param>
|
||||
/// <param name="psHost">The PowerShell host to use in Editor Services.</param>
|
||||
/// <param name="sessionDetailsPath">The path to use for the session details file.</param>
|
||||
/// <param name="bundledModulePath">The path to the modules bundled with Editor Services.</param>
|
||||
/// <param name="logPath">The path to be used for Editor Services' logging.</param>
|
||||
public EditorServicesConfig(
|
||||
HostInfo hostInfo,
|
||||
PSHost psHost,
|
||||
string sessionDetailsPath,
|
||||
string bundledModulePath,
|
||||
string logPath)
|
||||
{
|
||||
HostInfo = hostInfo;
|
||||
PSHost = psHost;
|
||||
SessionDetailsPath = sessionDetailsPath;
|
||||
BundledModulePath = bundledModulePath;
|
||||
LogPath = logPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The host description object.
|
||||
/// </summary>
|
||||
public HostInfo HostInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The PowerShell host used by Editor Services.
|
||||
/// </summary>
|
||||
public PSHost PSHost { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to use for the session details file.
|
||||
/// </summary>
|
||||
public string SessionDetailsPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the modules bundled with EditorServices.
|
||||
/// </summary>
|
||||
public string BundledModulePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to use for logging for Editor Services.
|
||||
/// </summary>
|
||||
public string LogPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Names of or paths to any additional modules to load on startup.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> AdditionalModules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flags of features to enable on startup.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> FeatureFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The console REPL experience to use in the Extension Terminal
|
||||
/// (including none to disable the Extension Terminal).
|
||||
/// </summary>
|
||||
public ConsoleReplKind ConsoleRepl { get; set; } = ConsoleReplKind.None;
|
||||
|
||||
/// <summary>
|
||||
/// Will suppress messages to PSHost (to prevent Stdio clobbering)
|
||||
/// </summary>
|
||||
public bool UseNullPSHostUI { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum log level to log events with. Defaults to warning but is usually overriden by the startup process.
|
||||
/// </summary>
|
||||
public PsesLogLevel LogLevel { get; set; } = PsesLogLevel.Warning;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the language server protocol transport to use.
|
||||
/// </summary>
|
||||
public ITransportConfig LanguageServiceTransport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the debug adapter protocol transport to use.
|
||||
/// </summary>
|
||||
public ITransportConfig DebugServiceTransport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// PowerShell profile locations for Editor Services to use for its profiles.
|
||||
/// If none are provided, these will be generated from the hosting PowerShell's profile paths.
|
||||
/// </summary>
|
||||
public ProfilePathConfig ProfilePaths { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The InitialSessionState to use when creating runspaces. LanguageMode can be set here.
|
||||
/// </summary>
|
||||
public InitialSessionState InitialSessionState { get; internal set; }
|
||||
|
||||
public string StartupBanner { get; set; } = @"
|
||||
|
||||
=====> PowerShell Editor Services <=====
|
||||
|
||||
";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for Editor Services' PowerShell profile paths.
|
||||
/// </summary>
|
||||
public struct ProfilePathConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The path to the profile shared by all users across all PowerShell hosts.
|
||||
/// </summary>
|
||||
public string AllUsersAllHosts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the profile shared by all users specific to this PSES host.
|
||||
/// </summary>
|
||||
public string AllUsersCurrentHost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the profile specific to the current user across all hosts.
|
||||
/// </summary>
|
||||
public string CurrentUserAllHosts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the profile specific to the current user and to this PSES host.
|
||||
/// </summary>
|
||||
public string CurrentUserCurrentHost { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple readonly object to describe basic host metadata.
|
||||
/// </summary>
|
||||
public class HostInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new host info object.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the host.</param>
|
||||
/// <param name="profileId">The profile ID of the host.</param>
|
||||
/// <param name="version">The version of the host.</param>
|
||||
public HostInfo(string name, string profileId, Version version)
|
||||
{
|
||||
Name = name;
|
||||
ProfileId = profileId;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the host.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The profile ID of the host.
|
||||
/// </summary>
|
||||
public string ProfileId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of the host.
|
||||
/// </summary>
|
||||
public Version Version { get; }
|
||||
}
|
||||
}
|
||||
356
src/PowerShellEditorServices.Hosting/Configuration/HostLogger.cs
Normal file
356
src/PowerShellEditorServices.Hosting/Configuration/HostLogger.cs
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Management.Automation.Host;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Log Level for HostLogger. This is a direct copy of LogLevel from Microsoft.Extensions.Logging, and will map to
|
||||
/// MEL.LogLevel once MEL is bootstrapped, but we don't want to load any MEL assemblies until the Assembly Load
|
||||
/// Context is set up.
|
||||
/// </summary>
|
||||
public enum PsesLogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs that contain the most detailed messages. These messages may contain sensitive application data.
|
||||
/// These messages are disabled by default and should never be enabled in a production environment.
|
||||
/// </summary>
|
||||
Trace = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Logs that are used for interactive investigation during development. These logs should primarily contain
|
||||
/// information useful for debugging and have no long-term value.
|
||||
/// </summary>
|
||||
Debug = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Logs that track the general flow of the application. These logs should have long-term value.
|
||||
/// </summary>
|
||||
Information = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the
|
||||
/// application execution to stop.
|
||||
/// </summary>
|
||||
Warning = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a
|
||||
/// failure in the current activity, not an application-wide failure.
|
||||
/// </summary>
|
||||
Error = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires
|
||||
/// immediate attention.
|
||||
/// </summary>
|
||||
Critical = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Not used for writing log messages. Specifies that a logging category should not write any messages.
|
||||
/// </summary>
|
||||
None = 6,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A logging front-end for host startup allowing handover to the backend and decoupling from
|
||||
/// the host's particular logging sink.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This custom logger exists to allow us to log during startup, which is vital information for
|
||||
/// debugging, but happens before we can load any logger library. This is because startup
|
||||
/// happens in our isolated assembly environment. See #2292 for more information.
|
||||
/// </remarks>
|
||||
public sealed class HostLogger :
|
||||
IObservable<(PsesLogLevel logLevel, string message)>,
|
||||
IObservable<(int logLevel, string message)>
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple translation struct to convert PsesLogLevel to an int for backend passthrough.
|
||||
/// </summary>
|
||||
private class LogObserver : IObserver<(PsesLogLevel logLevel, string message)>
|
||||
{
|
||||
private readonly IObserver<(int logLevel, string message)> _observer;
|
||||
|
||||
public LogObserver(IObserver<(int logLevel, string message)> observer) => _observer = observer;
|
||||
|
||||
public void OnCompleted() => _observer.OnCompleted();
|
||||
|
||||
public void OnError(Exception error) => _observer.OnError(error);
|
||||
|
||||
public void OnNext((PsesLogLevel logLevel, string message) value) => _observer.OnNext(((int)value.logLevel, value.message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple unsubscriber that allows subscribers to remove themselves from the observer list later.
|
||||
/// </summary>
|
||||
private class Unsubscriber : IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<IObserver<(PsesLogLevel, string)>, bool> _subscribedObservers;
|
||||
|
||||
private readonly IObserver<(PsesLogLevel, string)> _thisSubscriber;
|
||||
|
||||
public Unsubscriber(ConcurrentDictionary<IObserver<(PsesLogLevel, string)>, bool> subscribedObservers, IObserver<(PsesLogLevel, string)> thisSubscriber)
|
||||
{
|
||||
_subscribedObservers = subscribedObservers;
|
||||
_thisSubscriber = thisSubscriber;
|
||||
}
|
||||
|
||||
public void Dispose() => _subscribedObservers.TryRemove(_thisSubscriber, out bool _);
|
||||
}
|
||||
|
||||
private readonly PsesLogLevel _minimumLogLevel;
|
||||
|
||||
private readonly ConcurrentQueue<(PsesLogLevel logLevel, string message)> _logMessages;
|
||||
|
||||
// The bool value here is meaningless and ignored,
|
||||
// the ConcurrentDictionary just provides a way to efficiently keep track of subscribers across threads
|
||||
private readonly ConcurrentDictionary<IObserver<(PsesLogLevel logLevel, string message)>, bool> _observers;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new logger in the host.
|
||||
/// </summary>
|
||||
/// <param name="minimumLogLevel">The minimum log level to log.</param>
|
||||
public HostLogger(PsesLogLevel minimumLogLevel)
|
||||
{
|
||||
_minimumLogLevel = minimumLogLevel;
|
||||
_logMessages = new ConcurrentQueue<(PsesLogLevel logLevel, string message)>();
|
||||
_observers = new ConcurrentDictionary<IObserver<(PsesLogLevel logLevel, string message)>, bool>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe a new log sink.
|
||||
/// </summary>
|
||||
/// <param name="observer">The log sink to subscribe.</param>
|
||||
/// <returns>A disposable unsubscribe object.</returns>
|
||||
public IDisposable Subscribe(IObserver<(PsesLogLevel logLevel, string message)> observer)
|
||||
{
|
||||
if (observer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(observer));
|
||||
}
|
||||
|
||||
_observers[observer] = true;
|
||||
|
||||
// Catch up a late subscriber to messages already logged
|
||||
foreach ((PsesLogLevel logLevel, string message) entry in _logMessages)
|
||||
{
|
||||
observer.OnNext(entry);
|
||||
}
|
||||
|
||||
return new Unsubscriber(_observers, observer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe a new log sink.
|
||||
/// </summary>
|
||||
/// <param name="observer">The log sink to subscribe.</param>
|
||||
/// <returns>A disposable unsubscribe object.</returns>
|
||||
public IDisposable Subscribe(IObserver<(int logLevel, string message)> observer)
|
||||
{
|
||||
if (observer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(observer));
|
||||
}
|
||||
|
||||
return Subscribe(new LogObserver(observer));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log a message to log sinks.
|
||||
/// </summary>
|
||||
/// <param name="logLevel">The log severity level of message to log.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public void Log(PsesLogLevel logLevel, string message)
|
||||
{
|
||||
// Do nothing if the severity is lower than the minimum
|
||||
if (logLevel < _minimumLogLevel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remember this for later subscriptions
|
||||
_logMessages.Enqueue((logLevel, message));
|
||||
|
||||
// Send this log to all observers
|
||||
foreach (IObserver<(PsesLogLevel logLevel, string message)> observer in _observers.Keys)
|
||||
{
|
||||
observer.OnNext((logLevel, message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method for logging exceptions.
|
||||
/// </summary>
|
||||
/// <param name="message">The human-directed message to accompany the exception.</param>
|
||||
/// <param name="exception">The actual exception to log.</param>
|
||||
/// <param name="callerName">The name of the calling method.</param>
|
||||
/// <param name="callerSourceFile">The name of the file where this is logged.</param>
|
||||
/// <param name="callerLineNumber">The line in the file where this is logged.</param>
|
||||
public void LogException(
|
||||
string message,
|
||||
Exception exception,
|
||||
[CallerMemberName] string callerName = null,
|
||||
[CallerFilePath] string callerSourceFile = null,
|
||||
[CallerLineNumber] int callerLineNumber = -1) => Log(PsesLogLevel.Error, $"{message}. Exception logged in {callerSourceFile} on line {callerLineNumber} in {callerName}:\n{exception}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A log sink to direct log messages back to the PowerShell host.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that calling this through the cmdlet causes an error,
|
||||
/// so instead we log directly to the host.
|
||||
/// Since it's likely that the process will end when PSES shuts down,
|
||||
/// there's no good reason to need objects rather than writing directly to the host.
|
||||
/// </remarks>
|
||||
/// <param name="ui">The PowerShell host user interface object to log output to.</param>
|
||||
internal class PSHostLogger(PSHostUserInterface ui) : IObserver<(PsesLogLevel logLevel, string message)>
|
||||
{
|
||||
public void OnCompleted()
|
||||
{
|
||||
// No-op since there's nothing to close or dispose,
|
||||
// we just stop writing to the host
|
||||
}
|
||||
|
||||
public void OnError(Exception error) => OnNext((PsesLogLevel.Error, $"Error occurred while logging: {error}"));
|
||||
|
||||
public void OnNext((PsesLogLevel logLevel, string message) value)
|
||||
{
|
||||
(PsesLogLevel logLevel, string message) = value;
|
||||
switch (logLevel)
|
||||
{
|
||||
case PsesLogLevel.Trace:
|
||||
ui.WriteDebugLine("[Trace] " + message);
|
||||
break;
|
||||
case PsesLogLevel.Debug:
|
||||
ui.WriteDebugLine(message);
|
||||
break;
|
||||
case PsesLogLevel.Information:
|
||||
ui.WriteVerboseLine(message);
|
||||
break;
|
||||
case PsesLogLevel.Warning:
|
||||
ui.WriteWarningLine(message);
|
||||
break;
|
||||
case PsesLogLevel.Error:
|
||||
case PsesLogLevel.Critical:
|
||||
ui.WriteErrorLine(message);
|
||||
break;
|
||||
default:
|
||||
ui.WriteDebugLine("UNKNOWN:" + message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple log sink that logs to a stream, typically used to log to a file.
|
||||
/// </summary>
|
||||
internal class StreamLogger : IObserver<(PsesLogLevel logLevel, string message)>, IDisposable
|
||||
{
|
||||
public static StreamLogger CreateWithNewFile(string path)
|
||||
{
|
||||
FileStream fileStream = new(
|
||||
path,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.Read,
|
||||
bufferSize: 4096,
|
||||
FileOptions.SequentialScan);
|
||||
|
||||
return new StreamLogger(new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)));
|
||||
}
|
||||
|
||||
private readonly StreamWriter _fileWriter;
|
||||
|
||||
private readonly BlockingCollection<string> _messageQueue;
|
||||
|
||||
private readonly CancellationTokenSource _cancellationSource;
|
||||
|
||||
private readonly Thread _writerThread;
|
||||
|
||||
// This cannot be a bool
|
||||
// See https://stackoverflow.com/q/6164751
|
||||
private int _hasCompleted;
|
||||
|
||||
private IDisposable _unsubscriber;
|
||||
|
||||
public StreamLogger(StreamWriter streamWriter)
|
||||
{
|
||||
streamWriter.AutoFlush = true;
|
||||
_fileWriter = streamWriter;
|
||||
_hasCompleted = 0;
|
||||
_cancellationSource = new CancellationTokenSource();
|
||||
_messageQueue = new BlockingCollection<string>();
|
||||
|
||||
// Start writer listening to queue
|
||||
_writerThread = new Thread(RunWriter)
|
||||
{
|
||||
Name = "PSES Stream Logger Thread",
|
||||
};
|
||||
_writerThread.Start();
|
||||
}
|
||||
|
||||
public void OnCompleted()
|
||||
{
|
||||
// Ensure we only complete once
|
||||
if (Interlocked.Exchange(ref _hasCompleted, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cancellationSource.Cancel();
|
||||
_writerThread.Join();
|
||||
_unsubscriber.Dispose();
|
||||
_fileWriter.Flush();
|
||||
_fileWriter.Close();
|
||||
_fileWriter.Dispose();
|
||||
_cancellationSource.Dispose();
|
||||
_messageQueue.Dispose();
|
||||
}
|
||||
|
||||
public void OnError(Exception error) => OnNext((PsesLogLevel.Error, $"Error occurred while logging: {error}"));
|
||||
|
||||
public void OnNext((PsesLogLevel logLevel, string message) value)
|
||||
{
|
||||
string message = value.logLevel switch
|
||||
{
|
||||
// String interpolation often considered a logging sin is OK here because our filtering happens before.
|
||||
PsesLogLevel.Trace => $"[TRC]: {value.message}",
|
||||
PsesLogLevel.Debug => $"[DBG]: {value.message}",
|
||||
PsesLogLevel.Information => $"[INF]: {value.message}",
|
||||
PsesLogLevel.Warning => $"[WRN]: {value.message}",
|
||||
PsesLogLevel.Error => $"[ERR]: {value.message}",
|
||||
PsesLogLevel.Critical => $"[CRT]: {value.message}",
|
||||
_ => value.message,
|
||||
};
|
||||
|
||||
_messageQueue.Add(message);
|
||||
}
|
||||
|
||||
public void AddUnsubscriber(IDisposable unsubscriber) => _unsubscriber = unsubscriber;
|
||||
|
||||
public void Dispose() => OnCompleted();
|
||||
|
||||
private void RunWriter()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (string logMessage in _messageQueue.GetConsumingEnumerable(_cancellationSource.Token))
|
||||
{
|
||||
_fileWriter.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Management.Automation;
|
||||
using System.Text;
|
||||
using SMA = System.Management.Automation;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes the session file when the server is ready for a connection,
|
||||
/// so that the client can connect.
|
||||
/// </summary>
|
||||
public interface ISessionFileWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Write a session file describing a failed startup.
|
||||
/// </summary>
|
||||
/// <param name="reason">The reason for the startup failure.</param>
|
||||
void WriteSessionFailure(string reason);
|
||||
|
||||
/// <summary>
|
||||
/// Write a session file describing a successful startup.
|
||||
/// </summary>
|
||||
/// <param name="languageServiceTransport">The transport configuration for the LSP service.</param>
|
||||
/// <param name="debugAdapterTransport">The transport configuration for the debug adapter service.</param>
|
||||
void WriteSessionStarted(ITransportConfig languageServiceTransport, ITransportConfig debugAdapterTransport);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default session file writer, which uses PowerShell to write a session file.
|
||||
/// </summary>
|
||||
public sealed class SessionFileWriter : ISessionFileWriter
|
||||
{
|
||||
// Use BOM-less UTF-8 for session file
|
||||
private static readonly Encoding s_sessionFileEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
|
||||
private readonly HostLogger _logger;
|
||||
|
||||
private readonly string _sessionFilePath;
|
||||
|
||||
private readonly Version _powerShellVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new session file writer for the given session file path.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger to log actions with.</param>
|
||||
/// <param name="sessionFilePath">The path to write the session file path to.</param>
|
||||
/// <param name="powerShellVersion">The process's PowerShell version object.</param>
|
||||
public SessionFileWriter(HostLogger logger, string sessionFilePath, Version powerShellVersion)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionFilePath = sessionFilePath;
|
||||
_powerShellVersion = powerShellVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a startup failure to the session file.
|
||||
/// </summary>
|
||||
/// <param name="reason">The reason for the startup failure.</param>
|
||||
public void WriteSessionFailure(string reason)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Writing session failure");
|
||||
|
||||
Dictionary<string, object> sessionObject = new()
|
||||
{
|
||||
{ "status", "failed" },
|
||||
{ "reason", reason },
|
||||
};
|
||||
|
||||
WriteSessionObject(sessionObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a successful server startup to the session file.
|
||||
/// </summary>
|
||||
/// <param name="languageServiceTransport">The LSP service transport configuration.</param>
|
||||
/// <param name="debugAdapterTransport">The debug adapter transport configuration.</param>
|
||||
public void WriteSessionStarted(ITransportConfig languageServiceTransport, ITransportConfig debugAdapterTransport)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Writing session started");
|
||||
|
||||
Dictionary<string, object> sessionObject = new()
|
||||
{
|
||||
{ "status", "started" },
|
||||
};
|
||||
|
||||
if (languageServiceTransport is not null)
|
||||
{
|
||||
sessionObject["languageServiceTransport"] = languageServiceTransport.SessionFileTransportName;
|
||||
|
||||
if (languageServiceTransport.SessionFileEntries is not null)
|
||||
{
|
||||
foreach (KeyValuePair<string, object> sessionEntry in languageServiceTransport.SessionFileEntries)
|
||||
{
|
||||
sessionObject[$"languageService{sessionEntry.Key}"] = sessionEntry.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debugAdapterTransport is not null)
|
||||
{
|
||||
sessionObject["debugServiceTransport"] = debugAdapterTransport.SessionFileTransportName;
|
||||
|
||||
if (debugAdapterTransport.SessionFileEntries != null)
|
||||
{
|
||||
foreach (KeyValuePair<string, object> sessionEntry in debugAdapterTransport.SessionFileEntries)
|
||||
{
|
||||
sessionObject[$"debugService{sessionEntry.Key}"] = sessionEntry.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WriteSessionObject(sessionObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the object representing the session file to the file by serializing it as JSON.
|
||||
/// </summary>
|
||||
/// <param name="sessionObject">The dictionary representing the session file.</param>
|
||||
private void WriteSessionObject(Dictionary<string, object> sessionObject)
|
||||
{
|
||||
sessionObject["powerShellVersion"] = _powerShellVersion.ToString();
|
||||
|
||||
string psModulePath = Environment.GetEnvironmentVariable("PSModulePath");
|
||||
string content = null;
|
||||
using (SMA.PowerShell pwsh = SMA.PowerShell.Create(RunspaceMode.NewRunspace))
|
||||
{
|
||||
content = pwsh.AddCommand("ConvertTo-Json")
|
||||
.AddParameter("InputObject", sessionObject)
|
||||
.AddParameter("Depth", 10)
|
||||
.AddParameter("Compress")
|
||||
.Invoke<string>()[0];
|
||||
|
||||
// Runspace creation has a bug where it resets the PSModulePath,
|
||||
// which we must correct for
|
||||
Environment.SetEnvironmentVariable("PSModulePath", psModulePath);
|
||||
|
||||
File.WriteAllText(_sessionFilePath, content, s_sessionFileEncoding);
|
||||
}
|
||||
|
||||
_logger.Log(PsesLogLevel.Debug, $"Session file written to {_sessionFilePath} with content:\n{content}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration specifying an editor services protocol transport stream configuration.
|
||||
/// </summary>
|
||||
public interface ITransportConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Create, connect and return the configured transport streams.
|
||||
/// </summary>
|
||||
/// <returns>The connected transport streams. inStream and outStream may be the same stream for duplex streams.</returns>
|
||||
Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// The name of the transport endpoint for logging.
|
||||
/// </summary>
|
||||
string EndpointDetails { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the transport to record in the session file.
|
||||
/// </summary>
|
||||
string SessionFileTransportName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Extra entries to record in the session file.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, object> SessionFileEntries { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the standard input/output transport.
|
||||
/// </summary>
|
||||
public sealed class StdioTransportConfig : ITransportConfig
|
||||
{
|
||||
private readonly HostLogger _logger;
|
||||
|
||||
public StdioTransportConfig(HostLogger logger) => _logger = logger;
|
||||
|
||||
public string EndpointDetails => "<stdio>";
|
||||
|
||||
public string SessionFileTransportName => "Stdio";
|
||||
|
||||
public IReadOnlyDictionary<string, object> SessionFileEntries { get; }
|
||||
|
||||
public Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Connecting stdio streams");
|
||||
return Task.FromResult((Console.OpenStandardInput(), Console.OpenStandardOutput()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for a full duplex named pipe.
|
||||
/// </summary>
|
||||
public sealed class DuplexNamedPipeTransportConfig : ITransportConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a duplex named pipe transport config with an automatically generated pipe name.
|
||||
/// </summary>
|
||||
/// <returns>A new duplex named pipe transport configuration.</returns>
|
||||
public static DuplexNamedPipeTransportConfig Create(HostLogger logger) => new(logger, NamedPipeUtils.GenerateValidNamedPipeName());
|
||||
|
||||
/// <summary>
|
||||
/// Create a duplex named pipe transport config with the given pipe name.
|
||||
/// </summary>
|
||||
/// <returns>A new duplex named pipe transport configuration.</returns>
|
||||
public static DuplexNamedPipeTransportConfig Create(HostLogger logger, string pipeName)
|
||||
{
|
||||
if (pipeName == null)
|
||||
{
|
||||
return DuplexNamedPipeTransportConfig.Create(logger);
|
||||
}
|
||||
|
||||
return new DuplexNamedPipeTransportConfig(logger, pipeName);
|
||||
}
|
||||
|
||||
private readonly HostLogger _logger;
|
||||
|
||||
private readonly string _pipeName;
|
||||
|
||||
private DuplexNamedPipeTransportConfig(HostLogger logger, string pipeName)
|
||||
{
|
||||
_logger = logger;
|
||||
_pipeName = pipeName;
|
||||
SessionFileEntries = new Dictionary<string, object> { { "PipeName", NamedPipeUtils.GetNamedPipePath(pipeName) } };
|
||||
}
|
||||
|
||||
public string EndpointDetails => $"InOut pipe: {_pipeName}";
|
||||
|
||||
public string SessionFileTransportName => "NamedPipe";
|
||||
|
||||
public IReadOnlyDictionary<string, object> SessionFileEntries { get; }
|
||||
|
||||
public async Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Creating named pipe");
|
||||
NamedPipeServerStream namedPipe = NamedPipeUtils.CreateNamedPipe(_pipeName, PipeDirection.InOut);
|
||||
_logger.Log(PsesLogLevel.Trace, "Waiting for named pipe connection");
|
||||
await namedPipe.WaitForConnectionAsync().ConfigureAwait(false);
|
||||
_logger.Log(PsesLogLevel.Trace, "Named pipe connected");
|
||||
return (namedPipe, namedPipe);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for two simplex named pipes.
|
||||
/// </summary>
|
||||
public sealed class SimplexNamedPipeTransportConfig : ITransportConfig
|
||||
{
|
||||
private const string InPipePrefix = "in";
|
||||
private const string OutPipePrefix = "out";
|
||||
|
||||
/// <summary>
|
||||
/// Create a pair of simplex named pipes using generated names.
|
||||
/// </summary>
|
||||
/// <returns>A new simplex named pipe transport config.</returns>
|
||||
public static SimplexNamedPipeTransportConfig Create(HostLogger logger) => SimplexNamedPipeTransportConfig.Create(logger, NamedPipeUtils.GenerateValidNamedPipeName(new[] { InPipePrefix, OutPipePrefix }));
|
||||
|
||||
/// <summary>
|
||||
/// Create a pair of simplex named pipes using the given name as a base.
|
||||
/// </summary>
|
||||
/// <returns>A new simplex named pipe transport config.</returns>
|
||||
public static SimplexNamedPipeTransportConfig Create(HostLogger logger, string pipeNameBase)
|
||||
{
|
||||
if (pipeNameBase == null)
|
||||
{
|
||||
return SimplexNamedPipeTransportConfig.Create(logger);
|
||||
}
|
||||
|
||||
string inPipeName = $"{InPipePrefix}_{pipeNameBase}";
|
||||
string outPipeName = $"{OutPipePrefix}_{pipeNameBase}";
|
||||
|
||||
return SimplexNamedPipeTransportConfig.Create(logger, inPipeName, outPipeName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a pair of simplex named pipes using the given names.
|
||||
/// </summary>
|
||||
/// <returns>A new simplex named pipe transport config.</returns>
|
||||
public static SimplexNamedPipeTransportConfig Create(HostLogger logger, string inPipeName, string outPipeName) => new(logger, inPipeName, outPipeName);
|
||||
|
||||
private readonly HostLogger _logger;
|
||||
private readonly string _inPipeName;
|
||||
private readonly string _outPipeName;
|
||||
|
||||
private SimplexNamedPipeTransportConfig(HostLogger logger, string inPipeName, string outPipeName)
|
||||
{
|
||||
_logger = logger;
|
||||
_inPipeName = inPipeName;
|
||||
_outPipeName = outPipeName;
|
||||
|
||||
SessionFileEntries = new Dictionary<string, object>
|
||||
{
|
||||
{ "ReadPipeName", NamedPipeUtils.GetNamedPipePath(inPipeName) },
|
||||
{ "WritePipeName", NamedPipeUtils.GetNamedPipePath(outPipeName) },
|
||||
};
|
||||
}
|
||||
|
||||
public string EndpointDetails => $"In pipe: {_inPipeName} Out pipe: {_outPipeName}";
|
||||
|
||||
public string SessionFileTransportName => "NamedPipeSimplex";
|
||||
|
||||
public IReadOnlyDictionary<string, object> SessionFileEntries { get; }
|
||||
|
||||
public async Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Starting in pipe connection");
|
||||
NamedPipeServerStream inPipe = NamedPipeUtils.CreateNamedPipe(_inPipeName, PipeDirection.InOut);
|
||||
Task inPipeConnected = inPipe.WaitForConnectionAsync();
|
||||
|
||||
_logger.Log(PsesLogLevel.Trace, "Starting out pipe connection");
|
||||
NamedPipeServerStream outPipe = NamedPipeUtils.CreateNamedPipe(_outPipeName, PipeDirection.Out);
|
||||
Task outPipeConnected = outPipe.WaitForConnectionAsync();
|
||||
|
||||
_logger.Log(PsesLogLevel.Trace, "Wating for pipe connections");
|
||||
await Task.WhenAll(inPipeConnected, outPipeConnected).ConfigureAwait(false);
|
||||
|
||||
_logger.Log(PsesLogLevel.Trace, "Simplex named pipe transport connected");
|
||||
return (inPipe, outPipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
399
src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs
Normal file
399
src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using SMA = System.Management.Automation;
|
||||
using System.Management.Automation;
|
||||
using System.Management.Automation.Runspaces;
|
||||
|
||||
#if ASSEMBLY_LOAD_STACKTRACE
|
||||
using System.Diagnostics;
|
||||
#endif
|
||||
|
||||
#if CoreCLR
|
||||
using System.Runtime.Loader;
|
||||
#else
|
||||
using Microsoft.Win32;
|
||||
#endif
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to contain the loading behavior of Editor Services.
|
||||
/// In particular, this class wraps the point where Editor Services is safely loaded
|
||||
/// in a way that separates its dependencies from the calling context.
|
||||
/// </summary>
|
||||
public sealed class EditorServicesLoader : IDisposable
|
||||
{
|
||||
#if !CoreCLR
|
||||
// TODO: Well, we're saying we need 4.8 here but we're building for 4.6.2...
|
||||
// See https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed
|
||||
private const int Net48Version = 528040;
|
||||
|
||||
private static readonly string s_psesBaseDirPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
#endif
|
||||
|
||||
private static readonly string s_psesDependencyDirPath = Path.GetFullPath(
|
||||
Path.Combine(
|
||||
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
|
||||
"..",
|
||||
"Common"));
|
||||
|
||||
/// <summary>
|
||||
/// Create a new Editor Services loader.
|
||||
/// </summary>
|
||||
/// <param name="logger">The host logger to use.</param>
|
||||
/// <param name="hostConfig">The host configuration to start editor services with.</param>
|
||||
/// <param name="sessionDetailsPath">Path to the session file to create on startup or startup failure.</param>
|
||||
/// <param name="loggersToUnsubscribe">The loggers to unsubscribe form writing to the terminal.</param>
|
||||
public static EditorServicesLoader Create(
|
||||
HostLogger logger,
|
||||
EditorServicesConfig hostConfig,
|
||||
string sessionDetailsPath,
|
||||
IReadOnlyCollection<IDisposable> loggersToUnsubscribe)
|
||||
{
|
||||
if (logger is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
if (hostConfig is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(hostConfig));
|
||||
}
|
||||
|
||||
Version powerShellVersion = GetPSVersion();
|
||||
SessionFileWriter sessionFileWriter = new(logger, sessionDetailsPath, powerShellVersion);
|
||||
logger.Log(PsesLogLevel.Trace, "Session file writer created");
|
||||
|
||||
#if CoreCLR
|
||||
// In .NET Core, we add an event here to redirect dependency loading to the new AssemblyLoadContext we load PSES' dependencies into
|
||||
logger.Log(PsesLogLevel.Debug, "Adding AssemblyResolve event handler for new AssemblyLoadContext dependency loading");
|
||||
|
||||
PsesLoadContext psesLoadContext = new(s_psesDependencyDirPath);
|
||||
|
||||
if (hostConfig.LogLevel == PsesLogLevel.Trace)
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) =>
|
||||
{
|
||||
logger.Log(
|
||||
PsesLogLevel.Trace,
|
||||
$"Loaded into load context {AssemblyLoadContext.GetLoadContext(args.LoadedAssembly)}: {args.LoadedAssembly}");
|
||||
};
|
||||
}
|
||||
|
||||
AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext _, AssemblyName asmName) =>
|
||||
{
|
||||
#if ASSEMBLY_LOAD_STACKTRACE
|
||||
logger.Log(PsesLogLevel.Trace, $"Assembly resolve event fired for {asmName}. Stacktrace:\n{new StackTrace()}");
|
||||
#else
|
||||
logger.Log(PsesLogLevel.Trace, $"Assembly resolve event fired for {asmName}");
|
||||
#endif
|
||||
|
||||
// We only want the Editor Services DLL; the new ALC will lazily load its dependencies automatically
|
||||
if (!string.Equals(asmName.Name, "Microsoft.PowerShell.EditorServices", StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll");
|
||||
|
||||
logger.Log(PsesLogLevel.Debug, "Loading PSES DLL using new assembly load context");
|
||||
|
||||
return psesLoadContext.LoadFromAssemblyPath(asmPath);
|
||||
};
|
||||
#else
|
||||
// In .NET Framework we add an event here to redirect dependency loading in the current AppDomain for PSES' dependencies
|
||||
logger.Log(PsesLogLevel.Debug, "Adding AssemblyResolve event handler for dependency loading");
|
||||
|
||||
if (hostConfig.LogLevel == PsesLogLevel.Trace)
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) =>
|
||||
{
|
||||
if (args.LoadedAssembly.IsDynamic)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.Log(
|
||||
PsesLogLevel.Trace,
|
||||
$"Loaded '{args.LoadedAssembly.GetName()}' from '{args.LoadedAssembly.Location}'");
|
||||
};
|
||||
}
|
||||
|
||||
// Unlike in .NET Core, we need to be look for all dependencies in .NET Framework, not just PSES.dll
|
||||
AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs args) =>
|
||||
{
|
||||
#if ASSEMBLY_LOAD_STACKTRACE
|
||||
logger.Log(PsesLogLevel.Trace, $"Assembly resolve event fired for {args.Name}. Stacktrace:\n{new StackTrace()}");
|
||||
#else
|
||||
logger.Log(PsesLogLevel.Trace, $"Assembly resolve event fired for {args.Name}");
|
||||
#endif
|
||||
|
||||
AssemblyName asmName = new(args.Name);
|
||||
string dllName = $"{asmName.Name}.dll";
|
||||
|
||||
// First look for the required assembly in the .NET Framework DLL dir
|
||||
string baseDirAsmPath = Path.Combine(s_psesBaseDirPath, dllName);
|
||||
if (File.Exists(baseDirAsmPath))
|
||||
{
|
||||
logger.Log(PsesLogLevel.Trace, $"Loading {args.Name} from PSES base dir into LoadFile context");
|
||||
return Assembly.LoadFile(baseDirAsmPath);
|
||||
}
|
||||
|
||||
// Then look in the shared .NET Standard directory
|
||||
string asmPath = Path.Combine(s_psesDependencyDirPath, dllName);
|
||||
if (File.Exists(asmPath))
|
||||
{
|
||||
logger.Log(PsesLogLevel.Trace, $"Loading {args.Name} from PSES dependency dir into LoadFile context");
|
||||
return Assembly.LoadFile(asmPath);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
#endif
|
||||
|
||||
return new EditorServicesLoader(logger, hostConfig, sessionFileWriter, loggersToUnsubscribe, powerShellVersion);
|
||||
}
|
||||
|
||||
private readonly EditorServicesConfig _hostConfig;
|
||||
|
||||
private readonly ISessionFileWriter _sessionFileWriter;
|
||||
|
||||
private readonly HostLogger _logger;
|
||||
|
||||
private readonly IReadOnlyCollection<IDisposable> _loggersToUnsubscribe;
|
||||
|
||||
private readonly Version _powerShellVersion;
|
||||
|
||||
private EditorServicesRunner _editorServicesRunner;
|
||||
|
||||
private EditorServicesLoader(
|
||||
HostLogger logger,
|
||||
EditorServicesConfig hostConfig,
|
||||
ISessionFileWriter sessionFileWriter,
|
||||
IReadOnlyCollection<IDisposable> loggersToUnsubscribe,
|
||||
Version powerShellVersion)
|
||||
{
|
||||
_logger = logger;
|
||||
_hostConfig = hostConfig;
|
||||
_sessionFileWriter = sessionFileWriter;
|
||||
_loggersToUnsubscribe = loggersToUnsubscribe;
|
||||
_powerShellVersion = powerShellVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load Editor Services and its dependencies in an isolated way and start it.
|
||||
/// This method's returned task will end when Editor Services shuts down.
|
||||
/// </summary>
|
||||
public Task LoadAndRunEditorServicesAsync()
|
||||
{
|
||||
// Log important host information here
|
||||
LogHostInformation();
|
||||
|
||||
CheckPowerShellVersion();
|
||||
|
||||
#if !CoreCLR
|
||||
// Make sure the .NET Framework version supports .NET Standard 2.0
|
||||
CheckDotNetVersion();
|
||||
#endif
|
||||
|
||||
// Add the bundled modules to the PSModulePath
|
||||
// TODO: Why do we do this in addition to passing the bundled module path to the host?
|
||||
UpdatePSModulePath();
|
||||
|
||||
// Check to see if the configuration we have is valid
|
||||
ValidateConfiguration();
|
||||
|
||||
// Method with no implementation that forces the PSES assembly to load, triggering an AssemblyResolve event
|
||||
_logger.Log(PsesLogLevel.Information, "Loading PowerShell Editor Services Assemblies");
|
||||
LoadEditorServices();
|
||||
|
||||
_logger.Log(PsesLogLevel.Information, "Starting PowerShell Editor Services");
|
||||
|
||||
_editorServicesRunner = new EditorServicesRunner(_logger, _hostConfig, _sessionFileWriter, _loggersToUnsubscribe);
|
||||
|
||||
// The trigger method for Editor Services
|
||||
return Task.Run(_editorServicesRunner.RunUntilShutdown);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Loader disposed");
|
||||
_editorServicesRunner?.Dispose();
|
||||
|
||||
// TODO:
|
||||
// Remove assembly resolve events
|
||||
// This is not high priority, since the PSES process shouldn't be reused
|
||||
}
|
||||
|
||||
private static void LoadEditorServices() =>
|
||||
// This must be in its own method, since the actual load happens when the calling method is called
|
||||
// The call within this method is therefore a total no-op
|
||||
EditorServicesLoading.LoadEditorServicesForHost();
|
||||
|
||||
private void CheckPowerShellVersion()
|
||||
{
|
||||
PSLanguageMode languageMode = Runspace.DefaultRunspace.SessionStateProxy.LanguageMode;
|
||||
|
||||
_logger.Log(PsesLogLevel.Trace, $@"
|
||||
== PowerShell Details ==
|
||||
- PowerShell version: {_powerShellVersion}
|
||||
- Language mode: {languageMode}
|
||||
");
|
||||
|
||||
if ((_powerShellVersion < new Version(5, 1))
|
||||
|| (_powerShellVersion >= new Version(6, 0) && _powerShellVersion < new Version(7, 2)))
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Error, $"PowerShell {_powerShellVersion} is not supported, please update!");
|
||||
_sessionFileWriter.WriteSessionFailure("powerShellVersion");
|
||||
}
|
||||
|
||||
// TODO: Check if language mode still matters for support.
|
||||
}
|
||||
|
||||
#if !CoreCLR
|
||||
private void CheckDotNetVersion()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Debug, "Checking that .NET Framework version is at least 4.8");
|
||||
using RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Net Framework Setup\NDP\v4\Full");
|
||||
object netFxValue = key?.GetValue("Release");
|
||||
if (netFxValue == null || netFxValue is not int netFxVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Log(PsesLogLevel.Debug, $".NET registry version: {netFxVersion}");
|
||||
|
||||
if (netFxVersion < Net48Version)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Error, $".NET Framework {netFxVersion} is out-of-date, please install at least 4.8: https://dotnet.microsoft.com/en-us/download/dotnet-framework");
|
||||
_sessionFileWriter.WriteSessionFailure("dotNetVersion");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private void UpdatePSModulePath()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_hostConfig.BundledModulePath))
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "BundledModulePath not set, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
string psModulePath = Environment.GetEnvironmentVariable("PSModulePath").TrimEnd(Path.PathSeparator);
|
||||
if ($"{psModulePath}{Path.PathSeparator}".Contains($"{_hostConfig.BundledModulePath}{Path.PathSeparator}"))
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "BundledModulePath already set, skipping");
|
||||
return;
|
||||
}
|
||||
psModulePath = $"{psModulePath}{Path.PathSeparator}{_hostConfig.BundledModulePath}";
|
||||
Environment.SetEnvironmentVariable("PSModulePath", psModulePath);
|
||||
_logger.Log(PsesLogLevel.Trace, $"Updated PSModulePath to: '{psModulePath}'");
|
||||
}
|
||||
|
||||
private void LogHostInformation()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, $"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}");
|
||||
|
||||
_logger.Log(PsesLogLevel.Debug, $@"
|
||||
== Build Details ==
|
||||
- Editor Services version: {BuildInfo.BuildVersion}
|
||||
- Build origin: {BuildInfo.BuildOrigin}
|
||||
- Build commit: {BuildInfo.BuildCommit}
|
||||
- Build time: {BuildInfo.BuildTime}
|
||||
");
|
||||
|
||||
_logger.Log(PsesLogLevel.Debug, $@"
|
||||
== Host Startup Configuration Details ==
|
||||
- Host name: {_hostConfig.HostInfo.Name}
|
||||
- Host version: {_hostConfig.HostInfo.Version}
|
||||
- Host profile ID: {_hostConfig.HostInfo.ProfileId}
|
||||
- PowerShell host type: {_hostConfig.PSHost.GetType()}
|
||||
|
||||
- REPL setting: {_hostConfig.ConsoleRepl}
|
||||
- Session details path: {_hostConfig.SessionDetailsPath}
|
||||
- Bundled modules path: {_hostConfig.BundledModulePath}
|
||||
- Additional modules: {(_hostConfig.AdditionalModules == null ? "<null>" : string.Join(", ", _hostConfig.AdditionalModules))}
|
||||
- Feature flags: {(_hostConfig.FeatureFlags == null ? "<null>" : string.Join(", ", _hostConfig.FeatureFlags))}
|
||||
|
||||
- Log path: {_hostConfig.LogPath}
|
||||
- Minimum log level: {_hostConfig.LogLevel}
|
||||
|
||||
- Profile paths:
|
||||
+ AllUsersAllHosts: {_hostConfig.ProfilePaths.AllUsersAllHosts ?? "<null>"}
|
||||
+ AllUsersCurrentHost: {_hostConfig.ProfilePaths.AllUsersCurrentHost ?? "<null>"}
|
||||
+ CurrentUserAllHosts: {_hostConfig.ProfilePaths.CurrentUserAllHosts ?? "<null>"}
|
||||
+ CurrentUserCurrentHost: {_hostConfig.ProfilePaths.CurrentUserCurrentHost ?? "<null>"}
|
||||
");
|
||||
|
||||
_logger.Log(PsesLogLevel.Debug, $@"
|
||||
== Console Details ==
|
||||
- Console input encoding: {Console.InputEncoding.EncodingName}
|
||||
- Console output encoding: {Console.OutputEncoding.EncodingName}
|
||||
- PowerShell output encoding: {GetPSOutputEncoding()}
|
||||
");
|
||||
|
||||
_logger.Log(PsesLogLevel.Debug, $@"
|
||||
== Environment Details ==
|
||||
- OS description: {RuntimeInformation.OSDescription}
|
||||
- OS architecture: {RuntimeInformation.OSArchitecture}
|
||||
- Process bitness: {(Environment.Is64BitProcess ? "64" : "32")}
|
||||
");
|
||||
}
|
||||
|
||||
private static string GetPSOutputEncoding()
|
||||
{
|
||||
using SMA.PowerShell pwsh = SMA.PowerShell.Create();
|
||||
return pwsh.AddScript(
|
||||
"[System.Diagnostics.DebuggerHidden()]param() $OutputEncoding.EncodingName",
|
||||
useLocalScope: true).Invoke<string>()[0];
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "Checking user-defined configuration")]
|
||||
private void ValidateConfiguration()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Debug, "Validating configuration");
|
||||
|
||||
bool lspUsesStdio = _hostConfig.LanguageServiceTransport is StdioTransportConfig;
|
||||
bool debugUsesStdio = _hostConfig.DebugServiceTransport is StdioTransportConfig;
|
||||
|
||||
// Ensure LSP and Debug are not both Stdio
|
||||
if (lspUsesStdio && debugUsesStdio)
|
||||
{
|
||||
throw new ArgumentException("LSP and Debug transports cannot both use Stdio");
|
||||
}
|
||||
|
||||
if (_hostConfig.ConsoleRepl != ConsoleReplKind.None
|
||||
&& (lspUsesStdio || debugUsesStdio))
|
||||
{
|
||||
throw new ArgumentException("Cannot use the REPL with a Stdio protocol transport");
|
||||
}
|
||||
|
||||
if (_hostConfig.PSHost == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(_hostConfig.PSHost));
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1825:Avoid zero-length array allocations", Justification = "Cannot use Array.Empty, since it must work in net452")]
|
||||
private static Version GetPSVersion()
|
||||
{
|
||||
// In order to read the $PSVersionTable variable,
|
||||
// we are forced to create a new runspace to avoid concurrency issues,
|
||||
// which is expensive.
|
||||
// Rather than do that, we instead go straight to the source,
|
||||
// which is a static property, internal in WinPS and public in PS 6+
|
||||
return typeof(PSObject).Assembly
|
||||
.GetType("System.Management.Automation.PSVersionInfo")
|
||||
.GetMethod("get_PSVersion", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
.Invoke(null, new object[0]) as Version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "PSES is not localized", Scope = "module")]
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerShell.EditorServices.Server;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to manage the startup of PowerShell Editor Services.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should be called by <see cref="EditorServicesLoader"/> only after Editor Services has
|
||||
/// been loaded. It relies on <see cref="EditorServicesServerFactory"/> to indirectly load <see
|
||||
/// cref="Microsoft.Extensions.Logging"/> and <see
|
||||
/// cref="Microsoft.Extensions.DependencyInjection"/>.
|
||||
/// </remarks>
|
||||
internal class EditorServicesRunner : IDisposable
|
||||
{
|
||||
private readonly HostLogger _logger;
|
||||
|
||||
private readonly EditorServicesConfig _config;
|
||||
|
||||
private readonly ISessionFileWriter _sessionFileWriter;
|
||||
|
||||
private readonly EditorServicesServerFactory _serverFactory;
|
||||
|
||||
private readonly IReadOnlyCollection<IDisposable> _loggersToUnsubscribe;
|
||||
|
||||
private bool _alreadySubscribedDebug;
|
||||
|
||||
public EditorServicesRunner(
|
||||
HostLogger logger,
|
||||
EditorServicesConfig config,
|
||||
ISessionFileWriter sessionFileWriter,
|
||||
IReadOnlyCollection<IDisposable> loggersToUnsubscribe)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_sessionFileWriter = sessionFileWriter;
|
||||
// NOTE: This factory helps to isolate `Microsoft.Extensions.Logging/DependencyInjection`.
|
||||
_serverFactory = new(logger);
|
||||
_alreadySubscribedDebug = false;
|
||||
_loggersToUnsubscribe = loggersToUnsubscribe;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start and run Editor Services and then wait for shutdown.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO: Use "Async" suffix in names of methods that return an awaitable type.
|
||||
/// </remarks>
|
||||
/// <returns>A task that ends when Editor Services shuts down.</returns>
|
||||
public Task RunUntilShutdown()
|
||||
{
|
||||
// Start Editor Services (see function below)
|
||||
Task runAndAwaitShutdown = CreateEditorServicesAndRunUntilShutdown();
|
||||
|
||||
// Now write the session file
|
||||
_logger.Log(PsesLogLevel.Trace, "Writing session file");
|
||||
_sessionFileWriter.WriteSessionStarted(_config.LanguageServiceTransport, _config.DebugServiceTransport);
|
||||
|
||||
// Finally, wait for Editor Services to shut down
|
||||
_logger.Log(PsesLogLevel.Debug, "Waiting on PSES run/shutdown");
|
||||
return runAndAwaitShutdown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: This class probably should not be <see cref="IDisposable"/> as the primary
|
||||
/// intention of that interface is to provide cleanup of unmanaged resources, which the
|
||||
/// logger certainly is not. Nor is this class used with a <see langword="using"/>. It is
|
||||
/// only because of the use of <see cref="_serverFactory"/> that this class is also
|
||||
/// disposable, and instead that class should be fixed.
|
||||
/// </summary>
|
||||
public void Dispose() => _serverFactory.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// This is the servers' entry point, e.g. <c>main</c>, as it instantiates, runs and waits
|
||||
/// for the LSP and debug servers at the heart of Editor Services. Uses <see
|
||||
/// cref="HostStartupInfo"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The logical stack of the program is:
|
||||
/// <list type="number">
|
||||
/// <listheader>
|
||||
/// <term>Symbol</term>
|
||||
/// <description>Description</description>
|
||||
/// </listheader>
|
||||
/// <item>
|
||||
/// <term><see cref="Commands.StartEditorServicesCommand"/></term>
|
||||
/// <description>
|
||||
/// The StartEditorServicesCommand PSCmdlet, our PowerShell cmdlet written in C# and
|
||||
/// shipped in the module.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="Commands.StartEditorServicesCommand.EndProcessing"/></term>
|
||||
/// <description>
|
||||
/// As a cmdlet, this is the end of its "process" block, and it instantiates <see
|
||||
/// cref="EditorServicesLoader"/>.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="EditorServicesLoader.LoadAndRunEditorServicesAsync"></term>
|
||||
/// <description>
|
||||
/// Loads isolated dependencies then runs and returns the next task.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="RunUntilShutdown"></term>
|
||||
/// <description>Task which opens a logfile then returns this task.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="CreateEditorServicesAndRunUntilShutdown"></term>
|
||||
/// <description>This task!</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <returns>A task that ends when Editor Services shuts down.</returns>
|
||||
private async Task CreateEditorServicesAndRunUntilShutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Debug, "Creating/running editor services");
|
||||
|
||||
bool creatingLanguageServer = _config.LanguageServiceTransport != null;
|
||||
bool creatingDebugServer = _config.DebugServiceTransport != null;
|
||||
bool isTempDebugSession = creatingDebugServer && !creatingLanguageServer;
|
||||
|
||||
// Set up information required to instantiate servers
|
||||
HostStartupInfo hostStartupInfo = CreateHostStartupInfo();
|
||||
|
||||
// If we just want a temp debug session, run that and do nothing else
|
||||
if (isTempDebugSession)
|
||||
{
|
||||
await RunTempDebugSessionAsync(hostStartupInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Log(PsesLogLevel.Information, "PSES Startup Completed. Starting Language Server.");
|
||||
_logger.Log(PsesLogLevel.Information, "Please check the LSP log file in your client for further messages. In VSCode, this is the 'PowerShell' output pane");
|
||||
|
||||
// We want LSP and maybe debugging
|
||||
// To do that we:
|
||||
// - Create the LSP server
|
||||
// - Possibly kick off the debug server creation
|
||||
// - Start the LSP server
|
||||
// - Possibly start the debug server
|
||||
// - Wait for the LSP server to finish
|
||||
|
||||
// Unsubscribe the host logger here so that the Extension Terminal is not polluted with input after the first prompt
|
||||
if (_loggersToUnsubscribe != null)
|
||||
{
|
||||
foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe)
|
||||
{
|
||||
loggerToUnsubscribe.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
WriteStartupBanner();
|
||||
|
||||
PsesLanguageServer languageServer = await CreateLanguageServerAsync(hostStartupInfo).ConfigureAwait(false);
|
||||
|
||||
Task<PsesDebugServer> debugServerCreation = null;
|
||||
if (creatingDebugServer)
|
||||
{
|
||||
debugServerCreation = CreateDebugServerWithLanguageServerAsync(languageServer);
|
||||
}
|
||||
|
||||
Task languageServerStart = languageServer.StartAsync();
|
||||
|
||||
Task debugServerStart = null;
|
||||
if (creatingDebugServer)
|
||||
{
|
||||
// We don't need to wait for this to start, since we instead wait for it to complete later
|
||||
debugServerStart = StartDebugServer(debugServerCreation);
|
||||
}
|
||||
|
||||
await languageServerStart.ConfigureAwait(false);
|
||||
if (debugServerStart != null)
|
||||
{
|
||||
await debugServerStart.ConfigureAwait(false);
|
||||
}
|
||||
await languageServer.WaitForShutdown().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Resubscribe host logger to log shutdown events to the console
|
||||
_logger.Subscribe(new PSHostLogger(_config.PSHost.UI));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunTempDebugSessionAsync(HostStartupInfo hostDetails)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Information, "Starting temporary debug session");
|
||||
PsesDebugServer debugServer = await CreateDebugServerForTempSessionAsync(hostDetails).ConfigureAwait(false);
|
||||
_logger.Log(PsesLogLevel.Debug, "Debug server created");
|
||||
await debugServer.StartAsync().ConfigureAwait(false);
|
||||
_logger.Log(PsesLogLevel.Debug, "Debug server started");
|
||||
await debugServer.WaitForShutdown().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "It's a wrapper.")]
|
||||
private async Task StartDebugServer(Task<PsesDebugServer> debugServerCreation)
|
||||
{
|
||||
PsesDebugServer debugServer = await debugServerCreation.ConfigureAwait(false);
|
||||
|
||||
// When the debug server shuts down, we want it to automatically restart
|
||||
// To do this, we set an event to allow it to create a new debug server as its session ends
|
||||
if (!_alreadySubscribedDebug)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, "Subscribing debug server for session ended event");
|
||||
_alreadySubscribedDebug = true;
|
||||
debugServer.SessionEnded += DebugServer_OnSessionEnded;
|
||||
}
|
||||
|
||||
_logger.Log(PsesLogLevel.Trace, "Starting debug server");
|
||||
|
||||
await debugServer.StartAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task RestartDebugServerAsync(PsesDebugServer debugServer)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Debug, "Restarting debug server");
|
||||
Task<PsesDebugServer> debugServerCreation = RecreateDebugServerAsync(debugServer);
|
||||
return StartDebugServer(debugServerCreation);
|
||||
}
|
||||
|
||||
private async Task<PsesLanguageServer> CreateLanguageServerAsync(HostStartupInfo hostDetails)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, $"Creating LSP transport with endpoint {_config.LanguageServiceTransport.EndpointDetails}");
|
||||
(Stream inStream, Stream outStream) = await _config.LanguageServiceTransport.ConnectStreamsAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.Log(PsesLogLevel.Debug, "Creating language server");
|
||||
return _serverFactory.CreateLanguageServer(inStream, outStream, hostDetails);
|
||||
}
|
||||
|
||||
private async Task<PsesDebugServer> CreateDebugServerWithLanguageServerAsync(PsesLanguageServer languageServer)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Trace, $"Creating debug adapter transport with endpoint {_config.DebugServiceTransport.EndpointDetails}");
|
||||
(Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.Log(PsesLogLevel.Debug, "Creating debug adapter");
|
||||
return _serverFactory.CreateDebugServerWithLanguageServer(inStream, outStream, languageServer);
|
||||
}
|
||||
|
||||
private async Task<PsesDebugServer> RecreateDebugServerAsync(PsesDebugServer debugServer)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Debug, "Recreating debug adapter transport");
|
||||
(Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.Log(PsesLogLevel.Debug, "Recreating debug adapter");
|
||||
return _serverFactory.RecreateDebugServer(inStream, outStream, debugServer);
|
||||
}
|
||||
|
||||
private async Task<PsesDebugServer> CreateDebugServerForTempSessionAsync(HostStartupInfo hostDetails)
|
||||
{
|
||||
(Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false);
|
||||
|
||||
return _serverFactory.CreateDebugServerForTempSession(inStream, outStream, hostDetails);
|
||||
}
|
||||
|
||||
private HostStartupInfo CreateHostStartupInfo()
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Debug, "Creating startup info object");
|
||||
|
||||
ProfilePathInfo profilePaths = null;
|
||||
if (_config.ProfilePaths.AllUsersAllHosts != null
|
||||
|| _config.ProfilePaths.AllUsersCurrentHost != null
|
||||
|| _config.ProfilePaths.CurrentUserAllHosts != null
|
||||
|| _config.ProfilePaths.CurrentUserCurrentHost != null)
|
||||
{
|
||||
profilePaths = new ProfilePathInfo(
|
||||
_config.ProfilePaths.CurrentUserAllHosts,
|
||||
_config.ProfilePaths.CurrentUserCurrentHost,
|
||||
_config.ProfilePaths.AllUsersAllHosts,
|
||||
_config.ProfilePaths.AllUsersCurrentHost);
|
||||
}
|
||||
|
||||
return new HostStartupInfo(
|
||||
_config.HostInfo.Name,
|
||||
_config.HostInfo.ProfileId,
|
||||
_config.HostInfo.Version,
|
||||
_config.PSHost,
|
||||
profilePaths,
|
||||
_config.FeatureFlags,
|
||||
_config.AdditionalModules,
|
||||
_config.InitialSessionState,
|
||||
_config.LogPath,
|
||||
(int)_config.LogLevel, //This maps to MEL log levels, we use int so this is easily supplied externally.
|
||||
consoleReplEnabled: _config.ConsoleRepl != ConsoleReplKind.None,
|
||||
useNullPSHostUI: _config.UseNullPSHostUI,
|
||||
usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine,
|
||||
bundledModulePath: _config.BundledModulePath);
|
||||
}
|
||||
|
||||
private void WriteStartupBanner()
|
||||
{
|
||||
if (_config.ConsoleRepl == ConsoleReplKind.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_config.PSHost.UI.WriteLine(_config.StartupBanner);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "Intentionally fire and forget.")]
|
||||
private void DebugServer_OnSessionEnded(object sender, EventArgs args)
|
||||
{
|
||||
_logger.Log(PsesLogLevel.Debug, "Debug session ended, restarting debug service...");
|
||||
PsesDebugServer oldServer = (PsesDebugServer)sender;
|
||||
oldServer.Dispose();
|
||||
_alreadySubscribedDebug = false;
|
||||
Task.Run(() => RestartDebugServerAsync(oldServer));
|
||||
}
|
||||
}
|
||||
}
|
||||
151
src/PowerShellEditorServices.Hosting/Internal/NamedPipeUtils.cs
Normal file
151
src/PowerShellEditorServices.Hosting/Internal/NamedPipeUtils.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
|
||||
#if !CoreCLR
|
||||
using System.Security.Principal;
|
||||
using System.Security.AccessControl;
|
||||
#else
|
||||
using System.Runtime.InteropServices;
|
||||
#endif
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility class for handling named pipe creation in .NET Core and .NET Framework.
|
||||
/// </summary>
|
||||
internal static class NamedPipeUtils
|
||||
{
|
||||
#if !CoreCLR
|
||||
// .NET Framework requires the buffer size to be specified
|
||||
private const int PipeBufferSize = 1024;
|
||||
#endif
|
||||
|
||||
internal static NamedPipeServerStream CreateNamedPipe(
|
||||
string pipeName,
|
||||
PipeDirection pipeDirection)
|
||||
{
|
||||
#if CoreCLR
|
||||
return new NamedPipeServerStream(
|
||||
pipeName: pipeName,
|
||||
direction: pipeDirection,
|
||||
maxNumberOfServerInstances: 1,
|
||||
transmissionMode: PipeTransmissionMode.Byte,
|
||||
options: PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous);
|
||||
#else
|
||||
|
||||
// In .NET Framework, we must manually ACL the named pipes we create
|
||||
|
||||
PipeSecurity pipeSecurity = new();
|
||||
|
||||
WindowsIdentity identity = WindowsIdentity.GetCurrent();
|
||||
WindowsPrincipal principal = new(identity);
|
||||
|
||||
if (principal.IsInRole(WindowsBuiltInRole.Administrator))
|
||||
{
|
||||
// Allow the Administrators group full access to the pipe.
|
||||
pipeSecurity.AddAccessRule(
|
||||
new PipeAccessRule(
|
||||
new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, domainSid: null).Translate(typeof(NTAccount)),
|
||||
PipeAccessRights.FullControl, AccessControlType.Allow));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Allow the current user read/write access to the pipe.
|
||||
pipeSecurity.AddAccessRule(new PipeAccessRule(
|
||||
WindowsIdentity.GetCurrent().User,
|
||||
PipeAccessRights.ReadWrite, AccessControlType.Allow));
|
||||
}
|
||||
|
||||
return new NamedPipeServerStream(
|
||||
pipeName,
|
||||
pipeDirection,
|
||||
maxNumberOfServerInstances: 1,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous,
|
||||
inBufferSize: PipeBufferSize,
|
||||
outBufferSize: PipeBufferSize,
|
||||
pipeSecurity);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a named pipe name known to not already be in use.
|
||||
/// </summary>
|
||||
/// <param name="prefixes">Prefix variants of the pipename to test, if any.</param>
|
||||
/// <returns>A named pipe name or name suffix that is safe to you.</returns>
|
||||
public static string GenerateValidNamedPipeName(IReadOnlyCollection<string> prefixes = null)
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
string pipeName = $"PSES_{Path.GetRandomFileName()}";
|
||||
|
||||
// In the simple prefix-less case, just test the pipe name
|
||||
if (prefixes == null)
|
||||
{
|
||||
if (!IsPipeNameValid(pipeName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return pipeName;
|
||||
}
|
||||
|
||||
// If we have prefixes, test that all prefix/pipename combinations are valid
|
||||
bool allPipeNamesValid = true;
|
||||
foreach (string prefix in prefixes)
|
||||
{
|
||||
string prefixedPipeName = $"{prefix}_{pipeName}";
|
||||
if (!IsPipeNameValid(prefixedPipeName))
|
||||
{
|
||||
allPipeNamesValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allPipeNamesValid)
|
||||
{
|
||||
return pipeName;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Unable to create named pipe; no available names");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate that a named pipe file name is a legitimate named pipe file name and is not already in use.
|
||||
/// </summary>
|
||||
/// <param name="pipeName">The named pipe name to validate. This should be a simple name rather than a path.</param>
|
||||
/// <returns>True if the named pipe name is valid, false otherwise.</returns>
|
||||
public static bool IsPipeNameValid(string pipeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pipeName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !File.Exists(GetNamedPipePath(pipeName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the path of a named pipe given its name.
|
||||
/// </summary>
|
||||
/// <param name="pipeName">The simple name of the named pipe.</param>
|
||||
/// <returns>The full path of the named pipe.</returns>
|
||||
#pragma warning disable IDE0022
|
||||
public static string GetNamedPipePath(string pipeName)
|
||||
{
|
||||
#if CoreCLR
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{pipeName}");
|
||||
}
|
||||
#endif
|
||||
return $@"\\.\pipe\{pipeName}";
|
||||
}
|
||||
}
|
||||
#pragma warning restore IDE0022
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// An AssemblyLoadContext (ALC) designed to find PSES' dependencies in the given directory.
|
||||
/// This class only exists in .NET Core, where the ALC is used to isolate PSES' dependencies
|
||||
/// from the PowerShell assembly load context so that modules can import their own dependencies
|
||||
/// without issue in PSES.
|
||||
/// </summary>
|
||||
internal class PsesLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private static readonly string s_psHome = Path.GetDirectoryName(
|
||||
Assembly.GetEntryAssembly().Location);
|
||||
|
||||
private readonly string _dependencyDirPath;
|
||||
|
||||
public PsesLoadContext(string dependencyDirPath)
|
||||
{
|
||||
_dependencyDirPath = dependencyDirPath;
|
||||
|
||||
// Try and set our name in .NET Core 3+ for logging niceness
|
||||
TrySetName("PsesLoadContext");
|
||||
}
|
||||
|
||||
protected override Assembly Load(AssemblyName assemblyName)
|
||||
{
|
||||
// Since this class is responsible for loading any DLLs in .NET Core,
|
||||
// we must restrict the code in here to only use core types,
|
||||
// otherwise we may depend on assembly that we are trying to load and cause a StackOverflowException
|
||||
|
||||
// If we find the required assembly in $PSHOME, let another mechanism load the assembly
|
||||
string psHomeAsmPath = Path.Join(s_psHome, $"{assemblyName.Name}.dll");
|
||||
if (IsSatisfyingAssembly(assemblyName, psHomeAsmPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string asmPath = Path.Join(_dependencyDirPath, $"{assemblyName.Name}.dll");
|
||||
if (IsSatisfyingAssembly(assemblyName, asmPath))
|
||||
{
|
||||
return LoadFromAssemblyPath(asmPath);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Best effort; we must not throw if we fail")]
|
||||
private void TrySetName(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
// This field only exists in .NET Core 3+, but helps logging
|
||||
FieldInfo nameBackingField = typeof(AssemblyLoadContext).GetField(
|
||||
"_name",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
nameBackingField?.SetValue(this, name);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing -- we did our best
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSatisfyingAssembly(AssemblyName requiredAssemblyName, string assemblyPath)
|
||||
{
|
||||
if (!File.Exists(assemblyPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AssemblyName asmToLoadName = AssemblyName.GetAssemblyName(assemblyPath);
|
||||
|
||||
return string.Equals(asmToLoadName.Name, requiredAssemblyName.Name, StringComparison.OrdinalIgnoreCase)
|
||||
&& asmToLoadName.Version >= requiredAssemblyName.Version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import
|
||||
Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), PowerShellEditorServices.Common.props))\PowerShellEditorServices.Common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net462</TargetFrameworks>
|
||||
<AssemblyName>Microsoft.PowerShell.EditorServices.Hosting</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' != 'net462' ">
|
||||
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PowerShellStandard.Library" PrivateAssets="all" />
|
||||
<PackageReference Include="System.IO.Pipes.AccessControl" />
|
||||
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Roslynator">
|
||||
<PackageReference Include="Roslynator.Analyzers" />
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" />
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PowerShellEditorServices\PowerShellEditorServices.csproj" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
|
||||
<PackageReference Include="System.ValueTuple" />
|
||||
|
||||
<Compile Remove="Internal/PsesLoadContext.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Extension;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for managing the editor context from PSES extensions.
|
||||
/// </summary>
|
||||
public interface IEditorContextService
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the file context of the currently open file.
|
||||
/// </summary>
|
||||
/// <returns>The file context of the currently open file.</returns>
|
||||
Task<ILspCurrentFileContext> GetCurrentLspFileContextAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Open a fresh untitled file in the editor.
|
||||
/// </summary>
|
||||
/// <returns>A task that resolves when the file has been opened.</returns>
|
||||
Task OpenNewUntitledFileAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Open the given file in the editor.
|
||||
/// </summary>
|
||||
/// <param name="fileUri">The absolute URI to the file to open.</param>
|
||||
/// <returns>A task that resolves when the file has been opened.</returns>
|
||||
Task OpenFileAsync(Uri fileUri);
|
||||
|
||||
/// <summary>
|
||||
/// Open the given file in the editor.
|
||||
/// </summary>
|
||||
/// <param name="fileUri">The absolute URI to the file to open.</param>
|
||||
/// <param name="preview">If true, open the file as a preview.</param>
|
||||
/// <returns>A task that resolves when the file is opened.</returns>
|
||||
Task OpenFileAsync(Uri fileUri, bool preview);
|
||||
|
||||
/// <summary>
|
||||
/// Close the given file in the editor.
|
||||
/// </summary>
|
||||
/// <param name="fileUri">The absolute URI to the file to close.</param>
|
||||
/// <returns>A task that resolves when the file has been closed.</returns>
|
||||
Task CloseFileAsync(Uri fileUri);
|
||||
|
||||
/// <summary>
|
||||
/// Save the given file in the editor.
|
||||
/// </summary>
|
||||
/// <param name="fileUri">The absolute URI of the file to save.</param>
|
||||
/// <returns>A task that resolves when the file has been saved.</returns>
|
||||
Task SaveFileAsync(Uri fileUri);
|
||||
|
||||
/// <summary>
|
||||
/// Save the given file under a new name in the editor.
|
||||
/// </summary>
|
||||
/// <param name="oldFileUri">The absolute URI of the file to save.</param>
|
||||
/// <param name="newFileUri">The absolute URI of the location to save the file.</param>
|
||||
/// <returns></returns>
|
||||
Task SaveFileAsync(Uri oldFileUri, Uri newFileUri);
|
||||
|
||||
/// <summary>
|
||||
/// Set the selection in the currently focused editor window.
|
||||
/// </summary>
|
||||
/// <param name="range">The range in the file to select.</param>
|
||||
/// <returns>A task that resolves when the range has been selected.</returns>
|
||||
Task SetSelectionAsync(ILspFileRange range);
|
||||
|
||||
/// <summary>
|
||||
/// Insert text into a given file.
|
||||
/// </summary>
|
||||
/// <param name="fileUri">The absolute URI of the file to insert text into.</param>
|
||||
/// <param name="text">The text to insert into the file.</param>
|
||||
/// <param name="range">The range over which to insert the given text.</param>
|
||||
/// <returns>A task that resolves when the text has been inserted.</returns>
|
||||
Task InsertTextAsync(Uri fileUri, string text, ILspFileRange range);
|
||||
}
|
||||
|
||||
internal class EditorContextService : IEditorContextService
|
||||
{
|
||||
private readonly ILanguageServerFacade _languageServer;
|
||||
|
||||
internal EditorContextService(
|
||||
ILanguageServerFacade languageServer) => _languageServer = languageServer;
|
||||
|
||||
public async Task<ILspCurrentFileContext> GetCurrentLspFileContextAsync()
|
||||
{
|
||||
ClientEditorContext clientContext =
|
||||
await _languageServer.SendRequest(
|
||||
"editor/getEditorContext",
|
||||
new GetEditorContextRequest())
|
||||
.Returning<ClientEditorContext>(CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new LspCurrentFileContext(clientContext);
|
||||
}
|
||||
|
||||
public Task OpenNewUntitledFileAsync() => _languageServer.SendRequest<string>("editor/newFile", null).ReturningVoid(CancellationToken.None);
|
||||
|
||||
public Task OpenFileAsync(Uri fileUri) => OpenFileAsync(fileUri, preview: false);
|
||||
|
||||
public Task OpenFileAsync(Uri fileUri, bool preview)
|
||||
{
|
||||
return _languageServer.SendRequest("editor/openFile", new OpenFileDetails
|
||||
{
|
||||
FilePath = fileUri.LocalPath,
|
||||
Preview = preview,
|
||||
}).ReturningVoid(CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task CloseFileAsync(Uri fileUri) => _languageServer.SendRequest("editor/closeFile", fileUri.LocalPath).ReturningVoid(CancellationToken.None);
|
||||
|
||||
public Task SaveFileAsync(Uri fileUri) => SaveFileAsync(fileUri, null);
|
||||
|
||||
public Task SaveFileAsync(Uri oldFileUri, Uri newFileUri)
|
||||
{
|
||||
return _languageServer.SendRequest("editor/saveFile", new SaveFileDetails
|
||||
{
|
||||
FilePath = oldFileUri.LocalPath,
|
||||
NewPath = newFileUri?.LocalPath,
|
||||
}).ReturningVoid(CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task SetSelectionAsync(ILspFileRange range)
|
||||
{
|
||||
return _languageServer.SendRequest("editor/setSelection", new SetSelectionRequest
|
||||
{
|
||||
SelectionRange = range.ToOmnisharpRange()
|
||||
}).ReturningVoid(CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task InsertTextAsync(Uri fileUri, string text, ILspFileRange range)
|
||||
{
|
||||
return _languageServer.SendRequest("editor/insertText", new InsertTextRequest
|
||||
{
|
||||
FilePath = fileUri.LocalPath,
|
||||
InsertText = text,
|
||||
InsertRange = range.ToOmnisharpRange(),
|
||||
}).ReturningVoid(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Extension;
|
||||
using Microsoft.PowerShell.EditorServices.Utility;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
|
||||
|
||||
using InternalServices = Microsoft.PowerShell.EditorServices.Services;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Object to provide extension service APIs to extensions to PSES.
|
||||
/// </summary>
|
||||
public class EditorExtensionServiceProvider
|
||||
{
|
||||
private static readonly Assembly s_psesAsm = typeof(EditorExtensionServiceProvider).Assembly;
|
||||
|
||||
private static readonly Lazy<object> s_psesAsmLoadContextLazy = new(GetPsesAsmLoadContext);
|
||||
|
||||
private static readonly Lazy<Type> s_asmLoadContextType = new(() => Type.GetType("System.Runtime.Loader.AssemblyLoadContext"));
|
||||
|
||||
private static readonly Lazy<Func<IDisposable>> s_enterPsesReflectionContextLazy = new(GetPsesAlcReflectionContextEntryFunc);
|
||||
|
||||
private static readonly Lazy<Func<string, Assembly>> s_loadAssemblyInPsesAlc = new(GetPsesAlcLoadAsmFunc);
|
||||
|
||||
private static Type AsmLoadContextType => s_asmLoadContextType.Value;
|
||||
|
||||
private static object PsesAssemblyLoadContext => s_psesAsmLoadContextLazy.Value;
|
||||
|
||||
private static Func<IDisposable> EnterPsesAlcReflectionContext => s_enterPsesReflectionContextLazy.Value;
|
||||
|
||||
private static Func<string, Assembly> LoadAssemblyInPsesAlc => s_loadAssemblyInPsesAlc.Value;
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
internal EditorExtensionServiceProvider(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
LanguageServer = new LanguageServerService(_serviceProvider.GetService<ILanguageServerFacade>());
|
||||
ExtensionCommands = new ExtensionCommandService(_serviceProvider.GetService<ExtensionService>());
|
||||
Workspace = new WorkspaceService(_serviceProvider.GetService<InternalServices.WorkspaceService>());
|
||||
EditorContext = new EditorContextService(_serviceProvider.GetService<ILanguageServerFacade>());
|
||||
EditorUI = new EditorUIService(_serviceProvider.GetService<ILanguageServerFacade>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A service wrapper around the language server allowing sending notifications and requests to the LSP client.
|
||||
/// </summary>
|
||||
public ILanguageServerService LanguageServer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Service providing extension command registration and functionality.
|
||||
/// </summary>
|
||||
public IExtensionCommandService ExtensionCommands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Service providing editor workspace functionality.
|
||||
/// </summary>
|
||||
public IWorkspaceService Workspace { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Service providing current editor context functionality.
|
||||
/// </summary>
|
||||
public IEditorContextService EditorContext { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Service providing editor UI functionality.
|
||||
/// </summary>
|
||||
public IEditorUIService EditorUI { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get an underlying service object from PSES by type name.
|
||||
/// </summary>
|
||||
/// <param name="psesServiceFullTypeName">The full type name of the service to get.</param>
|
||||
/// <returns>The service object requested, or null if no service of that type name exists.</returns>
|
||||
/// <remarks>
|
||||
/// This method is intended as a trapdoor and should not be used in the first instance.
|
||||
/// Consider using the public extension services if possible.
|
||||
/// </remarks>
|
||||
public object GetService(string psesServiceFullTypeName) => GetService(psesServiceFullTypeName, "Microsoft.PowerShell.EditorServices");
|
||||
|
||||
/// <summary>
|
||||
/// Get an underlying service object from PSES by type name.
|
||||
/// </summary>
|
||||
/// <param name="fullTypeName">The full type name of the service to get.</param>
|
||||
/// <param name="assemblyName">The assembly name from which the service comes.</param>
|
||||
/// <returns>The service object requested, or null if no service of that type name exists.</returns>
|
||||
/// <remarks>
|
||||
/// This method is intended as a trapdoor and should not be used in the first instance.
|
||||
/// Consider using the public extension services if possible.
|
||||
/// </remarks>
|
||||
public object GetService(string fullTypeName, string assemblyName)
|
||||
{
|
||||
string asmQualifiedName = $"{fullTypeName}, {assemblyName}";
|
||||
return GetServiceByAssemblyQualifiedName(asmQualifiedName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a PSES service by its fully assembly qualified name.
|
||||
/// </summary>
|
||||
/// <param name="asmQualifiedTypeName">The fully assembly qualified name of the service type to load.</param>
|
||||
/// <returns>The service corresponding to the given type, or null if none was found.</returns>
|
||||
/// <remarks>
|
||||
/// It's not recommended to run this method in parallel with anything,
|
||||
/// since the global reflection context change may have side effects in other threads.
|
||||
/// </remarks>
|
||||
public object GetServiceByAssemblyQualifiedName(string asmQualifiedTypeName)
|
||||
{
|
||||
Type serviceType;
|
||||
if (VersionUtils.IsNetCore)
|
||||
{
|
||||
using (EnterPsesAlcReflectionContext())
|
||||
{
|
||||
serviceType = s_psesAsm.GetType(asmQualifiedTypeName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
serviceType = Type.GetType(asmQualifiedTypeName);
|
||||
}
|
||||
|
||||
return GetService(serviceType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an underlying service object from PSES by type name.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">The type of the service to fetch.</param>
|
||||
/// <returns>The service object requested, or null if no service of that type name exists.</returns>
|
||||
/// <remarks>
|
||||
/// This method is intended as a trapdoor and should not be used in the first instance.
|
||||
/// Consider using the public extension services if possible.
|
||||
/// Also note that services in PSES may live in a separate assembly load context,
|
||||
/// meaning that a type of the seemingly correct name may fail to fetch to a service
|
||||
/// that is known under a type of the same name but loaded in a different context.
|
||||
/// </remarks>
|
||||
public object GetService(Type serviceType) => _serviceProvider.GetService(serviceType);
|
||||
|
||||
/// <summary>
|
||||
/// Get the assembly load context the PSES loads its dependencies into.
|
||||
/// In .NET Framework, this returns null.
|
||||
/// </summary>
|
||||
/// <returns>The assembly load context used for loading PSES, or null in .NET Framework.</returns>
|
||||
public static object GetPsesAssemblyLoadContext()
|
||||
{
|
||||
if (!VersionUtils.IsNetCore)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return PsesAssemblyLoadContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load the given assembly in the PSES assembly load context.
|
||||
/// In .NET Framework, this simple loads the assembly in the LoadFrom context.
|
||||
/// </summary>
|
||||
/// <param name="assemblyPath">The absolute path of the assembly to load.</param>
|
||||
/// <returns>The loaded assembly object.</returns>
|
||||
public static Assembly LoadAssemblyInPsesLoadContext(string assemblyPath)
|
||||
{
|
||||
if (!VersionUtils.IsNetCore)
|
||||
{
|
||||
return Assembly.LoadFrom(assemblyPath);
|
||||
}
|
||||
|
||||
return LoadAssemblyInPsesAlc(assemblyPath);
|
||||
}
|
||||
|
||||
private static Func<IDisposable> GetPsesAlcReflectionContextEntryFunc()
|
||||
{
|
||||
MethodInfo enterReflectionContextMethod = AsmLoadContextType.GetMethod("EnterContextualReflection", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
return Expression.Lambda<Func<IDisposable>>(
|
||||
Expression.Convert(
|
||||
Expression.Call(Expression.Constant(PsesAssemblyLoadContext), enterReflectionContextMethod),
|
||||
typeof(IDisposable))).Compile();
|
||||
}
|
||||
|
||||
private static Func<string, Assembly> GetPsesAlcLoadAsmFunc()
|
||||
{
|
||||
MethodInfo loadFromAssemblyPathMethod = AsmLoadContextType.GetMethod("LoadFromAssemblyPath", BindingFlags.Public | BindingFlags.Instance);
|
||||
return (Func<string, Assembly>)loadFromAssemblyPathMethod.CreateDelegate(typeof(Func<string, Assembly>), PsesAssemblyLoadContext);
|
||||
}
|
||||
|
||||
private static object GetPsesAsmLoadContext()
|
||||
{
|
||||
if (!VersionUtils.IsNetCore)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type alcType = Type.GetType("System.Runtime.Loader.AssemblyLoadContext");
|
||||
MethodInfo getAlcMethod = alcType.GetMethod("GetLoadContext", BindingFlags.Public | BindingFlags.Static);
|
||||
return getAlcMethod.Invoke(obj: null, new object[] { s_psesAsm });
|
||||
}
|
||||
}
|
||||
}
|
||||
203
src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs
Normal file
203
src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Extension;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Object specifying a UI prompt option to display to the user.
|
||||
/// </summary>
|
||||
public sealed class PromptChoiceDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// Construct a prompt choice object for display in a prompt to the user.
|
||||
/// </summary>
|
||||
/// <param name="label">The label to identify this prompt choice. May not contain commas (',').</param>
|
||||
/// <param name="helpMessage">The message to display to users.</param>
|
||||
public PromptChoiceDetails(string label, string helpMessage)
|
||||
{
|
||||
if (label == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(label));
|
||||
}
|
||||
|
||||
// Currently VSCode sends back selected labels as a single string concatenated with ','
|
||||
// When this is fixed, we'll be able to allow commas in labels
|
||||
if (label.Contains(","))
|
||||
{
|
||||
throw new ArgumentException($"Labels may not contain ','. Label: '{label}'", nameof(label));
|
||||
}
|
||||
|
||||
Label = label;
|
||||
HelpMessage = helpMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The label to identify this prompt message.
|
||||
/// </summary>
|
||||
public string Label { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The message to display to users in the UI for this prompt choice.
|
||||
/// </summary>
|
||||
public string HelpMessage { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A service to manipulate the editor user interface.
|
||||
/// </summary>
|
||||
public interface IEditorUIService
|
||||
{
|
||||
/// <summary>
|
||||
/// Prompt input after displaying the given message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display with the prompt.</param>
|
||||
/// <returns>The input entered by the user, or null if the prompt was canceled.</returns>
|
||||
Task<string> PromptInputAsync(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Prompt a single selection from a set of choices.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display for the prompt.</param>
|
||||
/// <param name="choices">The choices to give the user.</param>
|
||||
/// <returns>The label of the selected choice, or null if the prompt was canceled.</returns>
|
||||
Task<string> PromptSelectionAsync(string message, IReadOnlyList<PromptChoiceDetails> choices);
|
||||
|
||||
/// <summary>
|
||||
/// Prompt a single selection from a set of choices.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display for the prompt.</param>
|
||||
/// <param name="choices">The choices to give the user.</param>
|
||||
/// <param name="defaultChoiceIndex">The index in the choice list of the default choice.</param>
|
||||
/// <returns>The label of the selected choice, or null if the prompt was canceled.</returns>
|
||||
Task<string> PromptSelectionAsync(string message, IReadOnlyList<PromptChoiceDetails> choices, int defaultChoiceIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Prompt a set of selections from a list of choices.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display for the prompt.</param>
|
||||
/// <param name="choices">The choices to give the user.</param>
|
||||
/// <returns>A list of the labels of selected choices, or null if the prompt was canceled.</returns>
|
||||
Task<IReadOnlyList<string>> PromptMultipleSelectionAsync(string message, IReadOnlyList<PromptChoiceDetails> choices);
|
||||
|
||||
/// <summary>
|
||||
/// Prompt a set of selections from a list of choices.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display for the prompt.</param>
|
||||
/// <param name="choices">The choices to give the user.</param>
|
||||
/// <param name="defaultChoiceIndexes">A list of the indexes of choices to mark as default.</param>
|
||||
/// <returns>A list of the labels of selected choices, or null if the prompt was canceled.</returns>
|
||||
Task<IReadOnlyList<string>> PromptMultipleSelectionAsync(string message, IReadOnlyList<PromptChoiceDetails> choices, IReadOnlyList<int> defaultChoiceIndexes);
|
||||
}
|
||||
|
||||
internal class EditorUIService : IEditorUIService
|
||||
{
|
||||
private static readonly string[] s_choiceResponseLabelSeparators = new[] { ", " };
|
||||
|
||||
private readonly ILanguageServerFacade _languageServer;
|
||||
|
||||
public EditorUIService(ILanguageServerFacade languageServer) => _languageServer = languageServer;
|
||||
|
||||
public async Task<string> PromptInputAsync(string message)
|
||||
{
|
||||
// The VSCode client currently doesn't use the Label field, so we ignore it
|
||||
ShowInputPromptResponse response = await _languageServer.SendRequest(
|
||||
"powerShell/showInputPrompt",
|
||||
new ShowInputPromptRequest
|
||||
{
|
||||
Name = message,
|
||||
})
|
||||
.Returning<ShowInputPromptResponse>(CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.PromptCancelled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.ResponseText;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<string>> PromptMultipleSelectionAsync(string message, IReadOnlyList<PromptChoiceDetails> choices) =>
|
||||
PromptMultipleSelectionAsync(message, choices, defaultChoiceIndexes: null);
|
||||
|
||||
public async Task<IReadOnlyList<string>> PromptMultipleSelectionAsync(string message, IReadOnlyList<PromptChoiceDetails> choices, IReadOnlyList<int> defaultChoiceIndexes)
|
||||
{
|
||||
ChoiceDetails[] choiceDetails = GetChoiceDetails(choices);
|
||||
|
||||
ShowChoicePromptResponse response = await _languageServer.SendRequest(
|
||||
"powerShell/showChoicePrompt",
|
||||
new ShowChoicePromptRequest
|
||||
{
|
||||
IsMultiChoice = true,
|
||||
Caption = string.Empty,
|
||||
Message = message,
|
||||
Choices = choiceDetails,
|
||||
DefaultChoices = defaultChoiceIndexes?.ToArray(),
|
||||
})
|
||||
.Returning<ShowChoicePromptResponse>(CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.PromptCancelled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.ResponseText.Split(s_choiceResponseLabelSeparators, StringSplitOptions.None);
|
||||
}
|
||||
|
||||
public Task<string> PromptSelectionAsync(string message, IReadOnlyList<PromptChoiceDetails> choices) =>
|
||||
PromptSelectionAsync(message, choices, defaultChoiceIndex: -1);
|
||||
|
||||
public async Task<string> PromptSelectionAsync(string message, IReadOnlyList<PromptChoiceDetails> choices, int defaultChoiceIndex)
|
||||
{
|
||||
ChoiceDetails[] choiceDetails = GetChoiceDetails(choices);
|
||||
|
||||
ShowChoicePromptResponse response = await _languageServer.SendRequest(
|
||||
"powerShell/showChoicePrompt",
|
||||
new ShowChoicePromptRequest
|
||||
{
|
||||
IsMultiChoice = false,
|
||||
Caption = string.Empty,
|
||||
Message = message,
|
||||
Choices = choiceDetails,
|
||||
DefaultChoices = defaultChoiceIndex > -1 ? new[] { defaultChoiceIndex } : Array.Empty<int>(),
|
||||
})
|
||||
.Returning<ShowChoicePromptResponse>(CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.PromptCancelled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.ResponseText;
|
||||
}
|
||||
|
||||
private static ChoiceDetails[] GetChoiceDetails(IReadOnlyList<PromptChoiceDetails> promptChoiceDetails)
|
||||
{
|
||||
ChoiceDetails[] choices = new ChoiceDetails[promptChoiceDetails.Count];
|
||||
for (int i = 0; i < promptChoiceDetails.Count; i++)
|
||||
{
|
||||
choices[i] = new ChoiceDetails
|
||||
{
|
||||
Label = promptChoiceDetails[i].Label,
|
||||
HelpMessage = promptChoiceDetails[i].HelpMessage,
|
||||
// There were intended to enable hotkey use for choice selections,
|
||||
// but currently VSCode does not do anything with them.
|
||||
// They can be exposed once VSCode supports them.
|
||||
HotKeyIndex = -1,
|
||||
HotKeyCharacter = null,
|
||||
};
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.PowerShell.EditorServices.Services.Extension;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for registration and invocation of extension commands.
|
||||
/// </summary>
|
||||
public interface IExtensionCommandService
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoke an extension command asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="commandName">The name of the extension command to invoke.</param>
|
||||
/// <param name="editorContext">The editor context in which to invoke the command.</param>
|
||||
/// <returns>A task that resolves when the command has been run.</returns>
|
||||
Task InvokeCommandAsync(string commandName, EditorContext editorContext);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new EditorCommand with the ExtensionService and
|
||||
/// causes its details to be sent to the host editor.
|
||||
/// </summary>
|
||||
/// <param name="editorCommand">The details about the editor command to be registered.</param>
|
||||
/// <returns>True if the command is newly registered, false if the command already exists.</returns>
|
||||
bool RegisterCommand(EditorCommand editorCommand);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters an existing EditorCommand based on its registered name.
|
||||
/// </summary>
|
||||
/// <param name="commandName">The name of the command to be unregistered.</param>
|
||||
void UnregisterCommand(string commandName);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all registered EditorCommands.
|
||||
/// </summary>
|
||||
/// <returns>A list of all registered EditorCommands.</returns>
|
||||
IReadOnlyList<EditorCommand> GetCommands();
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a new editor command is added.
|
||||
/// </summary>
|
||||
event EventHandler<EditorCommand> CommandAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an existing editor command is updated.
|
||||
/// </summary>
|
||||
event EventHandler<EditorCommand> CommandUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an existing editor command is removed.
|
||||
/// </summary>
|
||||
event EventHandler<EditorCommand> CommandRemoved;
|
||||
}
|
||||
|
||||
internal class ExtensionCommandService : IExtensionCommandService
|
||||
{
|
||||
private readonly ExtensionService _extensionService;
|
||||
|
||||
public ExtensionCommandService(ExtensionService extensionService)
|
||||
{
|
||||
_extensionService = extensionService;
|
||||
|
||||
_extensionService.CommandAdded += OnCommandAdded;
|
||||
_extensionService.CommandUpdated += OnCommandUpdated;
|
||||
_extensionService.CommandRemoved += OnCommandRemoved;
|
||||
}
|
||||
|
||||
public event EventHandler<EditorCommand> CommandAdded;
|
||||
|
||||
public event EventHandler<EditorCommand> CommandUpdated;
|
||||
|
||||
public event EventHandler<EditorCommand> CommandRemoved;
|
||||
|
||||
public IReadOnlyList<EditorCommand> GetCommands() => _extensionService.GetCommands();
|
||||
|
||||
public Task InvokeCommandAsync(string commandName, EditorContext editorContext) => _extensionService.InvokeCommandAsync(commandName, editorContext, CancellationToken.None);
|
||||
|
||||
public bool RegisterCommand(EditorCommand editorCommand) => _extensionService.RegisterCommand(editorCommand);
|
||||
|
||||
public void UnregisterCommand(string commandName) => _extensionService.UnregisterCommand(commandName);
|
||||
|
||||
private void OnCommandAdded(object sender, EditorCommand editorCommand) => CommandAdded?.Invoke(this, editorCommand);
|
||||
|
||||
private void OnCommandUpdated(object sender, EditorCommand editorCommand) => CommandUpdated?.Invoke(this, editorCommand);
|
||||
|
||||
private void OnCommandRemoved(object sender, EditorCommand editorCommand) => CommandRemoved?.Invoke(this, editorCommand);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service allowing the sending of notifications and requests to the PowerShell LSP language client from the server.
|
||||
/// </summary>
|
||||
public interface ILanguageServerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Send a parameterless notification.
|
||||
/// </summary>
|
||||
/// <param name="method">The method to send.</param>
|
||||
void SendNotification(string method);
|
||||
|
||||
/// <summary>
|
||||
/// Send a notification with parameters.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the parameter object.</typeparam>
|
||||
/// <param name="method">The method to send.</param>
|
||||
/// <param name="parameters">The parameters to send.</param>
|
||||
void SendNotification<T>(string method, T parameters);
|
||||
|
||||
/// <summary>
|
||||
/// Send a parameterless request with no response output.
|
||||
/// </summary>
|
||||
/// <param name="method">The method to send.</param>
|
||||
/// <returns>A task that resolves when the request is acknowledged.</returns>
|
||||
Task SendRequestAsync(string method);
|
||||
|
||||
/// <summary>
|
||||
/// Send a request with no response output.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the request parameter object.</typeparam>
|
||||
/// <param name="method">The method to send.</param>
|
||||
/// <param name="parameters">The request parameter object/body.</param>
|
||||
/// <returns>A task that resolves when the request is acknowledged.</returns>
|
||||
Task SendRequestAsync<T>(string method, T parameters);
|
||||
|
||||
/// <summary>
|
||||
/// Send a parameterless request and get its response.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">The type of the response expected.</typeparam>
|
||||
/// <param name="method">The method to send.</param>
|
||||
/// <returns>A task that resolves to the response sent by the server.</returns>
|
||||
Task<TResponse> SendRequestAsync<TResponse>(string method);
|
||||
|
||||
/// <summary>
|
||||
/// Send a request and get its response.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the parameter object.</typeparam>
|
||||
/// <typeparam name="TResponse">The type of the response expected.</typeparam>
|
||||
/// <param name="method">The method to send.</param>
|
||||
/// <param name="parameters">The parameters to send.</param>
|
||||
/// <returns>A task that resolves to the response sent by the server.</returns>
|
||||
Task<TResponse> SendRequestAsync<T, TResponse>(string method, T parameters);
|
||||
}
|
||||
|
||||
internal class LanguageServerService : ILanguageServerService
|
||||
{
|
||||
private readonly ILanguageServerFacade _languageServer;
|
||||
|
||||
internal LanguageServerService(ILanguageServerFacade languageServer) => _languageServer = languageServer;
|
||||
|
||||
public void SendNotification(string method) => _languageServer.SendNotification(method);
|
||||
|
||||
public void SendNotification<T>(string method, T parameters) => _languageServer.SendNotification(method, parameters);
|
||||
|
||||
public void SendNotification(string method, object parameters) => _languageServer.SendNotification(method, parameters);
|
||||
|
||||
public Task SendRequestAsync(string method) => _languageServer.SendRequest(method).ReturningVoid(CancellationToken.None);
|
||||
|
||||
public Task SendRequestAsync<T>(string method, T parameters) => _languageServer.SendRequest(method, parameters).ReturningVoid(CancellationToken.None);
|
||||
|
||||
public Task<TResponse> SendRequestAsync<TResponse>(string method) => _languageServer.SendRequest(method).Returning<TResponse>(CancellationToken.None);
|
||||
|
||||
public Task<TResponse> SendRequestAsync<T, TResponse>(string method, T parameters) => _languageServer.SendRequest(method, parameters).Returning<TResponse>(CancellationToken.None);
|
||||
|
||||
public Task<object> SendRequestAsync(string method, object parameters) => _languageServer.SendRequest(method, parameters).Returning<object>(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
153
src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs
Normal file
153
src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Management.Automation.Language;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// A script file in the current editor workspace.
|
||||
/// </summary>
|
||||
public interface IEditorScriptFile
|
||||
{
|
||||
/// <summary>
|
||||
/// The URI of the script file.
|
||||
/// </summary>
|
||||
Uri Uri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The text content of the file.
|
||||
/// </summary>
|
||||
string Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The lines of the file.
|
||||
/// </summary>
|
||||
IReadOnlyList<string> Lines { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The PowerShell AST of the script in the file.
|
||||
/// </summary>
|
||||
ScriptBlockAst Ast { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The PowerShell syntactic tokens of the script in the file.
|
||||
/// </summary>
|
||||
IReadOnlyList<Token> Tokens { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A service for querying and manipulating the editor workspace.
|
||||
/// </summary>
|
||||
public interface IWorkspaceService
|
||||
{
|
||||
/// <summary>
|
||||
/// The root path of the workspace for the current editor.
|
||||
/// </summary>
|
||||
string WorkspacePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the editor is configured to follow symlinks.
|
||||
/// </summary>
|
||||
bool FollowSymlinks { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of file globs to exclude from workspace management.
|
||||
/// </summary>
|
||||
IReadOnlyList<string> ExcludedFileGlobs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get a file within the workspace.
|
||||
/// </summary>
|
||||
/// <param name="fileUri">The absolute URI of the file to get.</param>
|
||||
/// <returns>A representation of the file.</returns>
|
||||
IEditorScriptFile GetFile(Uri fileUri);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to get a file within the workspace.
|
||||
/// </summary>
|
||||
/// <param name="fileUri">The absolute URI of the file to get.</param>
|
||||
/// <param name="file">The file, if it was found.</param>
|
||||
/// <returns>True if the file was found, false otherwise.</returns>
|
||||
bool TryGetFile(Uri fileUri, out IEditorScriptFile file);
|
||||
|
||||
/// <summary>
|
||||
/// Get all the open files in the editor workspace.
|
||||
/// The result is not kept up to date as files are opened or closed.
|
||||
/// </summary>
|
||||
/// <returns>All open files in the editor workspace.</returns>
|
||||
IReadOnlyList<IEditorScriptFile> GetOpenedFiles();
|
||||
}
|
||||
|
||||
internal class EditorScriptFile : IEditorScriptFile
|
||||
{
|
||||
private readonly ScriptFile _scriptFile;
|
||||
|
||||
internal EditorScriptFile(
|
||||
ScriptFile scriptFile)
|
||||
{
|
||||
_scriptFile = scriptFile;
|
||||
Uri = scriptFile.DocumentUri.ToUri();
|
||||
Lines = _scriptFile.FileLines.AsReadOnly();
|
||||
}
|
||||
|
||||
public Uri Uri { get; }
|
||||
|
||||
public IReadOnlyList<string> Lines { get; }
|
||||
|
||||
public string Content => _scriptFile.Contents;
|
||||
|
||||
public ScriptBlockAst Ast => _scriptFile.ScriptAst;
|
||||
|
||||
public IReadOnlyList<Token> Tokens => _scriptFile.ScriptTokens;
|
||||
}
|
||||
|
||||
internal class WorkspaceService : IWorkspaceService
|
||||
{
|
||||
private readonly EditorServices.Services.WorkspaceService _workspaceService;
|
||||
|
||||
internal WorkspaceService(
|
||||
EditorServices.Services.WorkspaceService workspaceService)
|
||||
{
|
||||
_workspaceService = workspaceService;
|
||||
ExcludedFileGlobs = _workspaceService.ExcludeFilesGlob.AsReadOnly();
|
||||
}
|
||||
|
||||
// TODO: This needs to use the associated EditorContext to get the workspace for the current
|
||||
// editor instead of the initial working directory.
|
||||
public string WorkspacePath => _workspaceService.InitialWorkingDirectory;
|
||||
|
||||
public bool FollowSymlinks => _workspaceService.FollowSymlinks;
|
||||
|
||||
public IReadOnlyList<string> ExcludedFileGlobs { get; }
|
||||
|
||||
public IEditorScriptFile GetFile(Uri fileUri) => GetEditorFileFromScriptFile(_workspaceService.GetFile(fileUri));
|
||||
|
||||
public bool TryGetFile(Uri fileUri, out IEditorScriptFile file)
|
||||
{
|
||||
if (!_workspaceService.TryGetFile(fileUri.LocalPath, out ScriptFile scriptFile))
|
||||
{
|
||||
file = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
file = GetEditorFileFromScriptFile(scriptFile);
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyList<IEditorScriptFile> GetOpenedFiles()
|
||||
{
|
||||
List<IEditorScriptFile> files = new();
|
||||
foreach (ScriptFile openedFile in _workspaceService.GetOpenedFiles())
|
||||
{
|
||||
files.Add(GetEditorFileFromScriptFile(openedFile));
|
||||
}
|
||||
return files.AsReadOnly();
|
||||
}
|
||||
|
||||
private static IEditorScriptFile GetEditorFileFromScriptFile(ScriptFile file) => new EditorScriptFile(file);
|
||||
}
|
||||
}
|
||||
86
src/PowerShellEditorServices/Extensions/EditorCommand.cs
Normal file
86
src/PowerShellEditorServices/Extensions/EditorCommand.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Management.Automation;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides details about a command that has been registered
|
||||
/// with the editor.
|
||||
/// </summary>
|
||||
public sealed class EditorCommand
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name which uniquely identifies the command.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display name for the command.
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the boolean which determines whether this command's
|
||||
/// output should be suppressed.
|
||||
/// </summary>
|
||||
public bool SuppressOutput { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ScriptBlock which can be used to execute the command.
|
||||
/// </summary>
|
||||
public ScriptBlock ScriptBlock { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new EditorCommand instance that invokes a cmdlet or
|
||||
/// function by name.
|
||||
/// </summary>
|
||||
/// <param name="commandName">The unique identifier name for the command.</param>
|
||||
/// <param name="displayName">The display name for the command.</param>
|
||||
/// <param name="suppressOutput">If true, causes output to be suppressed for this command.</param>
|
||||
/// <param name="cmdletName">The name of the cmdlet or function which will be invoked by this command.</param>
|
||||
public EditorCommand(
|
||||
string commandName,
|
||||
string displayName,
|
||||
bool suppressOutput,
|
||||
string cmdletName)
|
||||
: this(
|
||||
commandName,
|
||||
displayName,
|
||||
suppressOutput,
|
||||
ScriptBlock.Create(
|
||||
string.Format(
|
||||
"param($context) {0} $context",
|
||||
cmdletName)))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new EditorCommand instance that invokes a ScriptBlock.
|
||||
/// </summary>
|
||||
/// <param name="commandName">The unique identifier name for the command.</param>
|
||||
/// <param name="displayName">The display name for the command.</param>
|
||||
/// <param name="suppressOutput">If true, causes output to be suppressed for this command.</param>
|
||||
/// <param name="scriptBlock">The ScriptBlock which will be invoked by this command.</param>
|
||||
public EditorCommand(
|
||||
string commandName,
|
||||
string displayName,
|
||||
bool suppressOutput,
|
||||
ScriptBlock scriptBlock)
|
||||
{
|
||||
Name = commandName;
|
||||
DisplayName = displayName;
|
||||
SuppressOutput = suppressOutput;
|
||||
ScriptBlock = scriptBlock;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an attribute that can be used to target PowerShell
|
||||
/// commands for import as editor commands.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class EditorCommandAttribute : Attribute
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name which uniquely identifies the command.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display name for the command.
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this command's output
|
||||
/// should be suppressed.
|
||||
/// </summary>
|
||||
public bool SuppressOutput { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
100
src/PowerShellEditorServices/Extensions/EditorContext.cs
Normal file
100
src/PowerShellEditorServices/Extensions/EditorContext.cs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides context for the host editor at the time of creation.
|
||||
/// </summary>
|
||||
public sealed class EditorContext
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
private readonly IEditorOperations editorOperations;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FileContext for the active file.
|
||||
/// </summary>
|
||||
public FileContext CurrentFile { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the BufferRange representing the current selection in the file.
|
||||
/// </summary>
|
||||
public IFileRange SelectedRange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FilePosition representing the current cursor position.
|
||||
/// </summary>
|
||||
public IFilePosition CursorPosition { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the EditorContext class.
|
||||
/// </summary>
|
||||
/// <param name="editorOperations">An IEditorOperations implementation which performs operations in the editor.</param>
|
||||
/// <param name="currentFile">The ScriptFile that is in the active editor buffer.</param>
|
||||
/// <param name="cursorPosition">The position of the user's cursor in the active editor buffer.</param>
|
||||
/// <param name="selectedRange">The range of the user's selection in the active editor buffer.</param>
|
||||
/// <param name="language">Determines the language of the file.false If it is not specified, then it defaults to "Unknown"</param>
|
||||
internal EditorContext(
|
||||
IEditorOperations editorOperations,
|
||||
ScriptFile currentFile,
|
||||
BufferPosition cursorPosition,
|
||||
BufferRange selectedRange,
|
||||
string language = "Unknown")
|
||||
{
|
||||
this.editorOperations = editorOperations;
|
||||
CurrentFile = new FileContext(currentFile, this, editorOperations, language);
|
||||
SelectedRange = new BufferFileRange(selectedRange);
|
||||
CursorPosition = new BufferFilePosition(cursorPosition);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Sets a selection in the host editor's active buffer.
|
||||
/// </summary>
|
||||
/// <param name="startLine">The 1-based starting line of the selection.</param>
|
||||
/// <param name="startColumn">The 1-based starting column of the selection.</param>
|
||||
/// <param name="endLine">The 1-based ending line of the selection.</param>
|
||||
/// <param name="endColumn">The 1-based ending column of the selection.</param>
|
||||
public void SetSelection(
|
||||
int startLine,
|
||||
int startColumn,
|
||||
int endLine,
|
||||
int endColumn)
|
||||
{
|
||||
SetSelection(
|
||||
new FileRange(
|
||||
new FilePosition(startLine, startColumn),
|
||||
new FilePosition(endLine, endColumn)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a selection in the host editor's active buffer.
|
||||
/// </summary>
|
||||
/// <param name="startPosition">The starting position of the selection.</param>
|
||||
/// <param name="endPosition">The ending position of the selection.</param>
|
||||
public void SetSelection(FilePosition startPosition, FilePosition endPosition) => SetSelection(new FileRange(startPosition, endPosition));
|
||||
|
||||
/// <summary>
|
||||
/// Sets a selection in the host editor's active buffer.
|
||||
/// </summary>
|
||||
/// <param name="selectionRange">The range of the selection.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void SetSelection(FileRange selectionRange) => editorOperations.SetSelectionAsync(selectionRange.ToBufferRange()).Wait();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
482
src/PowerShellEditorServices/Extensions/EditorFileRanges.cs
Normal file
482
src/PowerShellEditorServices/Extensions/EditorFileRanges.cs
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.PowerShell.EditorServices.Services.Extension;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
using System;
|
||||
using System.Management.Automation.Language;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
public class FileScriptPosition : IScriptPosition, IFilePosition
|
||||
{
|
||||
public static FileScriptPosition Empty { get; } = new FileScriptPosition(null, 0, 0, 0);
|
||||
|
||||
public static FileScriptPosition FromPosition(FileContext file, int lineNumber, int columnNumber)
|
||||
{
|
||||
if (file is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
int currLine = 1;
|
||||
string fileText = file.Ast.Extent.Text;
|
||||
while (offset < fileText.Length && currLine < lineNumber)
|
||||
{
|
||||
offset = fileText.IndexOf('\n', offset) + 1;
|
||||
if (offset is 0)
|
||||
{
|
||||
// Line and column passed were not valid and the offset can not be determined.
|
||||
return new FileScriptPosition(file, lineNumber, columnNumber, offset);
|
||||
}
|
||||
|
||||
currLine++;
|
||||
}
|
||||
|
||||
offset += columnNumber - 1;
|
||||
|
||||
return new FileScriptPosition(file, lineNumber, columnNumber, offset);
|
||||
}
|
||||
|
||||
public static FileScriptPosition FromOffset(FileContext file, int offset)
|
||||
{
|
||||
if (file is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
}
|
||||
|
||||
int line = 1;
|
||||
string fileText = file.Ast.Extent.Text;
|
||||
|
||||
if (offset >= fileText.Length)
|
||||
{
|
||||
throw new ArgumentException("Offset greater than file length", nameof(offset));
|
||||
}
|
||||
|
||||
int lastLineOffset = -1;
|
||||
for (int i = 0; i < offset; i++)
|
||||
{
|
||||
if (fileText[i] == '\n')
|
||||
{
|
||||
lastLineOffset = i;
|
||||
line++;
|
||||
}
|
||||
}
|
||||
|
||||
int column = offset - lastLineOffset;
|
||||
|
||||
return new FileScriptPosition(file, line, column, offset);
|
||||
}
|
||||
|
||||
private readonly FileContext _file;
|
||||
|
||||
internal FileScriptPosition(FileContext file, int lineNumber, int columnNumber, int offset)
|
||||
{
|
||||
_file = file;
|
||||
Line = file?.GetTextLines()?[lineNumber - 1] ?? string.Empty;
|
||||
ColumnNumber = columnNumber;
|
||||
LineNumber = lineNumber;
|
||||
Offset = offset;
|
||||
}
|
||||
|
||||
public int ColumnNumber { get; }
|
||||
|
||||
public string File { get; }
|
||||
|
||||
public string Line { get; }
|
||||
|
||||
public int LineNumber { get; }
|
||||
|
||||
public int Offset { get; }
|
||||
|
||||
int IFilePosition.Column => ColumnNumber;
|
||||
|
||||
int IFilePosition.Line => LineNumber;
|
||||
|
||||
public string GetFullScript() => _file?.GetText() ?? string.Empty;
|
||||
}
|
||||
|
||||
public class FileScriptExtent : IScriptExtent, IFileRange
|
||||
{
|
||||
public static bool IsEmpty(FileScriptExtent extent)
|
||||
{
|
||||
return extent == Empty
|
||||
|| (extent.StartOffset == 0 && extent.EndOffset == 0);
|
||||
}
|
||||
|
||||
public static FileScriptExtent Empty { get; } = new FileScriptExtent(null, FileScriptPosition.Empty, FileScriptPosition.Empty);
|
||||
|
||||
public static FileScriptExtent FromOffsets(FileContext file, int startOffset, int endOffset)
|
||||
{
|
||||
if (file is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
}
|
||||
|
||||
return new FileScriptExtent(
|
||||
file,
|
||||
FileScriptPosition.FromOffset(file, startOffset),
|
||||
FileScriptPosition.FromOffset(file, endOffset));
|
||||
}
|
||||
|
||||
public static FileScriptExtent FromPositions(FileContext file, int startLine, int startColumn, int endLine, int endColumn)
|
||||
{
|
||||
if (file is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
}
|
||||
|
||||
return new FileScriptExtent(
|
||||
file,
|
||||
FileScriptPosition.FromPosition(file, startLine, startColumn),
|
||||
FileScriptPosition.FromPosition(file, endLine, endColumn));
|
||||
}
|
||||
|
||||
private readonly FileContext _file;
|
||||
private readonly FileScriptPosition _start;
|
||||
private readonly FileScriptPosition _end;
|
||||
|
||||
public FileScriptExtent(FileContext file, FileScriptPosition start, FileScriptPosition end)
|
||||
{
|
||||
_file = file;
|
||||
_start = start;
|
||||
_end = end;
|
||||
}
|
||||
|
||||
public int EndColumnNumber => _end.ColumnNumber;
|
||||
|
||||
public int EndLineNumber => _end.LineNumber;
|
||||
|
||||
public int EndOffset => _end.Offset;
|
||||
|
||||
public IScriptPosition EndScriptPosition => _end;
|
||||
|
||||
public string File => _file?.Path ?? string.Empty;
|
||||
|
||||
public int StartColumnNumber => _start.ColumnNumber;
|
||||
|
||||
public int StartLineNumber => _start.LineNumber;
|
||||
|
||||
public int StartOffset => _start.Offset;
|
||||
|
||||
public IScriptPosition StartScriptPosition => _start;
|
||||
|
||||
public string Text => _file?.GetText()?.Substring(_start.Offset, _end.Offset - _start.Offset) ?? string.Empty;
|
||||
|
||||
IFilePosition IFileRange.Start => _start;
|
||||
|
||||
IFilePosition IFileRange.End => _end;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 1-based file position, referring to a point in a file.
|
||||
/// </summary>
|
||||
public interface IFilePosition
|
||||
{
|
||||
/// <summary>
|
||||
/// The line number of the file position.
|
||||
/// </summary>
|
||||
int Line { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The column number of the file position.
|
||||
/// </summary>
|
||||
int Column { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 1-based file range, referring to a range within a file.
|
||||
/// </summary>
|
||||
public interface IFileRange
|
||||
{
|
||||
/// <summary>
|
||||
/// The start position of the range.
|
||||
/// </summary>
|
||||
IFilePosition Start { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The end position of the range.
|
||||
/// </summary>
|
||||
IFilePosition End { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot of a file, including the URI of the file
|
||||
/// and its textual contents when accessed.
|
||||
/// </summary>
|
||||
public interface IFileContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The URI of the file.
|
||||
/// </summary>
|
||||
Uri Uri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The content of the file when it was accessed.
|
||||
/// </summary>
|
||||
string Content { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 0-based position within a file, conformant with the Language Server Protocol.
|
||||
/// </summary>
|
||||
public interface ILspFilePosition
|
||||
{
|
||||
/// <summary>
|
||||
/// The line index of the position within the file.
|
||||
/// </summary>
|
||||
int Line { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The character offset from the line of the position.
|
||||
/// </summary>
|
||||
int Character { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 0-based range within a file, conformant with the Language Server Protocol.
|
||||
/// </summary>
|
||||
public interface ILspFileRange
|
||||
{
|
||||
/// <summary>
|
||||
/// The start position of the range.
|
||||
/// </summary>
|
||||
ILspFilePosition Start { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The end position of the range.
|
||||
/// </summary>
|
||||
ILspFilePosition End { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of a file in focus in the editor.
|
||||
/// </summary>
|
||||
public interface ILspCurrentFileContext : IFileContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The language the editor associates with this file.
|
||||
/// </summary>
|
||||
string Language { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The position of the cursor within the file when it was accessed.
|
||||
/// If the cursor is not in the file, values may be negative.
|
||||
/// </summary>
|
||||
ILspFilePosition CursorPosition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected range when the file was accessed.
|
||||
/// If no selection is made, values may be negative.
|
||||
/// </summary>
|
||||
ILspFileRange SelectionRange { get; }
|
||||
}
|
||||
|
||||
internal readonly struct OmnisharpLspPosition : ILspFilePosition, IEquatable<OmnisharpLspPosition>
|
||||
{
|
||||
private readonly Position _position;
|
||||
|
||||
public OmnisharpLspPosition(Position position) => _position = position;
|
||||
|
||||
public int Line => _position.Line;
|
||||
|
||||
public int Character => _position.Character;
|
||||
|
||||
public bool Equals(OmnisharpLspPosition other) => _position == other._position;
|
||||
}
|
||||
|
||||
internal readonly struct OmnisharpLspRange : ILspFileRange, IEquatable<OmnisharpLspRange>
|
||||
{
|
||||
private readonly Range _range;
|
||||
|
||||
public OmnisharpLspRange(Range range) => _range = range;
|
||||
|
||||
public ILspFilePosition Start => new OmnisharpLspPosition(_range.Start);
|
||||
|
||||
public ILspFilePosition End => new OmnisharpLspPosition(_range.End);
|
||||
|
||||
public bool Equals(OmnisharpLspRange other) => _range == other._range;
|
||||
}
|
||||
|
||||
internal readonly struct BufferFilePosition : IFilePosition, IEquatable<BufferFilePosition>
|
||||
{
|
||||
private readonly BufferPosition _position;
|
||||
|
||||
public BufferFilePosition(BufferPosition position) => _position = position;
|
||||
|
||||
public int Line => _position.Line;
|
||||
|
||||
public int Column => _position.Column;
|
||||
|
||||
public bool Equals(BufferFilePosition other)
|
||||
{
|
||||
return _position == other._position
|
||||
|| _position.Equals(other._position);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct BufferFileRange : IFileRange, IEquatable<BufferFileRange>
|
||||
{
|
||||
private readonly BufferRange _range;
|
||||
|
||||
public BufferFileRange(BufferRange range) => _range = range;
|
||||
|
||||
public IFilePosition Start => new BufferFilePosition(_range.Start);
|
||||
|
||||
public IFilePosition End => new BufferFilePosition(_range.End);
|
||||
|
||||
public bool Equals(BufferFileRange other)
|
||||
{
|
||||
return _range == other._range
|
||||
|| _range.Equals(other._range);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 1-based file position.
|
||||
/// </summary>
|
||||
public class FilePosition : IFilePosition
|
||||
{
|
||||
public FilePosition(int line, int column)
|
||||
{
|
||||
Line = line;
|
||||
Column = column;
|
||||
}
|
||||
|
||||
public int Line { get; }
|
||||
|
||||
public int Column { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 0-based file position.
|
||||
/// </summary>
|
||||
public class LspFilePosition : ILspFilePosition
|
||||
{
|
||||
public LspFilePosition(int line, int column)
|
||||
{
|
||||
Line = line;
|
||||
Character = column;
|
||||
}
|
||||
|
||||
public int Line { get; }
|
||||
|
||||
public int Character { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 1-based file range.
|
||||
/// </summary>
|
||||
public class FileRange : IFileRange
|
||||
{
|
||||
public FileRange(IFilePosition start, IFilePosition end)
|
||||
: this(start, end, file: null)
|
||||
{
|
||||
}
|
||||
|
||||
public FileRange(IFilePosition start, IFilePosition end, string file)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
File = file;
|
||||
}
|
||||
|
||||
public IFilePosition Start { get; }
|
||||
|
||||
public IFilePosition End { get; }
|
||||
|
||||
public string File { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 0-based file range.
|
||||
/// </summary>
|
||||
public class LspFileRange : ILspFileRange
|
||||
{
|
||||
public LspFileRange(ILspFilePosition start, ILspFilePosition end)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
public ILspFilePosition Start { get; }
|
||||
|
||||
public ILspFilePosition End { get; }
|
||||
}
|
||||
|
||||
internal class LspCurrentFileContext : ILspCurrentFileContext
|
||||
{
|
||||
private readonly ClientEditorContext _editorContext;
|
||||
|
||||
public LspCurrentFileContext(ClientEditorContext editorContext)
|
||||
{
|
||||
_editorContext = editorContext;
|
||||
Uri = new Uri(editorContext.CurrentFilePath);
|
||||
}
|
||||
|
||||
public string Language => _editorContext.CurrentFileLanguage;
|
||||
|
||||
public ILspFilePosition CursorPosition => new OmnisharpLspPosition(_editorContext.CursorPosition);
|
||||
|
||||
public ILspFileRange SelectionRange => new OmnisharpLspRange(_editorContext.SelectionRange);
|
||||
|
||||
public Uri Uri { get; }
|
||||
|
||||
public string Content => _editorContext.CurrentFileContent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods to conveniently convert between file position and range types.
|
||||
/// </summary>
|
||||
public static class FileObjectExtensionMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert a 1-based file position to a 0-based file position.
|
||||
/// </summary>
|
||||
/// <param name="position">The 1-based file position to convert.</param>
|
||||
/// <returns>An equivalent 0-based file position.</returns>
|
||||
public static ILspFilePosition ToLspPosition(this IFilePosition position) => new LspFilePosition(position.Line - 1, position.Column - 1);
|
||||
|
||||
/// <summary>
|
||||
/// Convert a 1-based file range to a 0-based file range.
|
||||
/// </summary>
|
||||
/// <param name="range">The 1-based file range to convert.</param>
|
||||
/// <returns>An equivalent 0-based file range.</returns>
|
||||
public static ILspFileRange ToLspRange(this IFileRange range) => new LspFileRange(range.Start.ToLspPosition(), range.End.ToLspPosition());
|
||||
|
||||
/// <summary>
|
||||
/// Convert a 0-based file position to a 1-based file position.
|
||||
/// </summary>
|
||||
/// <param name="position">The 0-based file position to convert.</param>
|
||||
/// <returns>An equivalent 1-based file position.</returns>
|
||||
public static IFilePosition ToFilePosition(this ILspFilePosition position) => new FilePosition(position.Line + 1, position.Character + 1);
|
||||
|
||||
/// <summary>
|
||||
/// Convert a 0-based file range to a 1-based file range.
|
||||
/// </summary>
|
||||
/// <param name="range">The 0-based file range to convert.</param>
|
||||
/// <returns>An equivalent 1-based file range.</returns>
|
||||
public static IFileRange ToFileRange(this ILspFileRange range) => new FileRange(range.Start.ToFilePosition(), range.End.ToFilePosition());
|
||||
|
||||
internal static bool HasRange(this IFileRange range)
|
||||
{
|
||||
return range.Start.Line != 0
|
||||
&& range.Start.Column != 0
|
||||
&& range.End.Line != 0
|
||||
&& range.End.Column != 0;
|
||||
}
|
||||
internal static ILspFilePosition ToLspPosition(this Position position) => new OmnisharpLspPosition(position);
|
||||
|
||||
internal static ILspFileRange ToLspRange(this Range range) => new OmnisharpLspRange(range);
|
||||
|
||||
internal static Position ToOmnisharpPosition(this ILspFilePosition position) => new(position.Line, position.Character);
|
||||
|
||||
internal static Range ToOmnisharpRange(this ILspFileRange range) => new(range.Start.ToOmnisharpPosition(), range.End.ToOmnisharpPosition());
|
||||
|
||||
internal static BufferPosition ToBufferPosition(this IFilePosition position) => new(position.Line, position.Column);
|
||||
|
||||
internal static BufferRange ToBufferRange(this IFileRange range) => new(range.Start.ToBufferPosition(), range.End.ToBufferPosition());
|
||||
}
|
||||
}
|
||||
127
src/PowerShellEditorServices/Extensions/EditorObject.cs
Normal file
127
src/PowerShellEditorServices/Extensions/EditorObject.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerShell.EditorServices.Extensions.Services;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Extension;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension class to access the editor API with.
|
||||
/// This is done so that the async/ALC APIs aren't exposed to PowerShell, where they're likely only to cause problems.
|
||||
/// </summary>
|
||||
public static class EditorObjectExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the provider of extension services for .NET extension tooling.
|
||||
/// </summary>
|
||||
/// <param name="editorObject">The editor object ($psEditor).</param>
|
||||
/// <returns>The extension services provider.</returns>
|
||||
public static EditorExtensionServiceProvider GetExtensionServiceProvider(this EditorObject editorObject) => editorObject.Api;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides the entry point of the extensibility API, inserted into
|
||||
/// the PowerShell session as the "$psEditor" variable.
|
||||
/// </summary>
|
||||
public class EditorObject
|
||||
{
|
||||
private static readonly TaskCompletionSource<bool> s_editorObjectReady = new();
|
||||
|
||||
/// <summary>
|
||||
/// A reference to the editor object instance. Only valid when <see cref="EditorObjectReady"/> completes.
|
||||
/// </summary>
|
||||
public static EditorObject Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A task that completes when the editor object static instance has been set.
|
||||
/// </summary>
|
||||
public static Task EditorObjectReady => s_editorObjectReady.Task;
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private readonly ExtensionService _extensionService;
|
||||
private readonly IEditorOperations _editorOperations;
|
||||
private readonly Lazy<EditorExtensionServiceProvider> _apiLazy;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
internal EditorExtensionServiceProvider Api => _apiLazy.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version of PowerShell Editor Services.
|
||||
/// </summary>
|
||||
public Version EditorServicesVersion => GetType().GetTypeInfo().Assembly.GetName().Version;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the workspace interface for the editor API.
|
||||
/// </summary>
|
||||
public EditorWorkspace Workspace { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the window interface for the editor API.
|
||||
/// </summary>
|
||||
public EditorWindow Window { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the EditorObject class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The service provider?</param>
|
||||
/// <param name="extensionService">An ExtensionService which handles command registration.</param>
|
||||
/// <param name="editorOperations">An IEditorOperations implementation which handles operations in the host editor.</param>
|
||||
internal EditorObject(
|
||||
IServiceProvider serviceProvider,
|
||||
ExtensionService extensionService,
|
||||
IEditorOperations editorOperations)
|
||||
{
|
||||
_extensionService = extensionService;
|
||||
_editorOperations = editorOperations;
|
||||
|
||||
// Create API area objects
|
||||
Workspace = new EditorWorkspace(_editorOperations);
|
||||
Window = new EditorWindow(_editorOperations);
|
||||
|
||||
// Create this lazily so that dependency injection does not have a circular call dependency
|
||||
_apiLazy = new Lazy<EditorExtensionServiceProvider>(() => new EditorExtensionServiceProvider(serviceProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new command in the editor.
|
||||
/// </summary>
|
||||
/// <param name="editorCommand">The EditorCommand to be registered.</param>
|
||||
/// <returns>True if the command is newly registered, false if the command already exists.</returns>
|
||||
public bool RegisterCommand(EditorCommand editorCommand) => _extensionService.RegisterCommand(editorCommand);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters an existing EditorCommand based on its registered name.
|
||||
/// </summary>
|
||||
/// <param name="commandName">The name of the command to be unregistered.</param>
|
||||
public void UnregisterCommand(string commandName) => _extensionService.UnregisterCommand(commandName);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all registered EditorCommands.
|
||||
/// </summary>
|
||||
/// <returns>An Array of all registered EditorCommands.</returns>
|
||||
public EditorCommand[] GetCommands() => _extensionService.GetCommands();
|
||||
/// <summary>
|
||||
/// Gets the EditorContext which contains the state of the editor
|
||||
/// at the time this method is invoked.
|
||||
/// </summary>
|
||||
/// <returns>A instance of the EditorContext class.</returns>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public EditorContext GetEditorContext() => _editorOperations.GetEditorContextAsync().Result;
|
||||
|
||||
internal void SetAsStaticInstance()
|
||||
{
|
||||
Instance = this;
|
||||
s_editorObjectReady.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/PowerShellEditorServices/Extensions/EditorRequests.cs
Normal file
73
src/PowerShellEditorServices/Extensions/EditorRequests.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
internal class ExtensionCommandAddedNotification
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string DisplayName { get; set; }
|
||||
}
|
||||
|
||||
internal class ExtensionCommandUpdatedNotification
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
internal class ExtensionCommandRemovedNotification
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
internal class GetEditorContextRequest
|
||||
{ }
|
||||
|
||||
internal enum EditorOperationResponse
|
||||
{
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
|
||||
internal class InsertTextRequest
|
||||
{
|
||||
public string FilePath { get; set; }
|
||||
|
||||
public string InsertText { get; set; }
|
||||
|
||||
public Range InsertRange { get; set; }
|
||||
}
|
||||
|
||||
internal class SetSelectionRequest
|
||||
{
|
||||
public Range SelectionRange { get; set; }
|
||||
}
|
||||
|
||||
internal class SetCursorPositionRequest
|
||||
{
|
||||
public Position CursorPosition { get; set; }
|
||||
}
|
||||
|
||||
internal class OpenFileDetails
|
||||
{
|
||||
public string FilePath { get; set; }
|
||||
|
||||
public bool Preview { get; set; }
|
||||
}
|
||||
|
||||
internal class SaveFileDetails
|
||||
{
|
||||
public string FilePath { get; set; }
|
||||
|
||||
public string NewPath { get; set; }
|
||||
}
|
||||
|
||||
internal class StatusBarMessageDetails
|
||||
{
|
||||
public string Message { get; set; }
|
||||
|
||||
public int? Timeout { get; set; }
|
||||
}
|
||||
}
|
||||
37
src/PowerShellEditorServices/Extensions/EditorTerminal.cs
Normal file
37
src/PowerShellEditorServices/Extensions/EditorTerminal.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a PowerShell-facing API which allows scripts to
|
||||
/// interact with the editor's terminal.
|
||||
/// </summary>
|
||||
public class EditorTerminal
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
private readonly IEditorOperations editorOperations;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the EditorTerminal class.
|
||||
/// </summary>
|
||||
/// <param name="editorOperations">An IEditorOperations implementation which handles operations in the host editor.</param>
|
||||
internal EditorTerminal(IEditorOperations editorOperations) => this.editorOperations = editorOperations;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Triggers to the editor to clear the terminal.
|
||||
/// </summary>
|
||||
public void Clear() => editorOperations.ClearTerminal();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
80
src/PowerShellEditorServices/Extensions/EditorWindow.cs
Normal file
80
src/PowerShellEditorServices/Extensions/EditorWindow.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a PowerShell-facing API which allows scripts to
|
||||
/// interact with the editor's window.
|
||||
/// </summary>
|
||||
public sealed class EditorWindow
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
private readonly IEditorOperations editorOperations;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the terminal interface for the editor API.
|
||||
/// </summary>
|
||||
public EditorTerminal Terminal { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the EditorWindow class.
|
||||
/// </summary>
|
||||
/// <param name="editorOperations">An IEditorOperations implementation which handles operations in the host editor.</param>
|
||||
internal EditorWindow(IEditorOperations editorOperations)
|
||||
{
|
||||
this.editorOperations = editorOperations;
|
||||
Terminal = new EditorTerminal(editorOperations);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Shows an informational message to the user.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be shown.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void ShowInformationMessage(string message) => editorOperations.ShowInformationMessageAsync(message).Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Shows an error message to the user.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be shown.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void ShowErrorMessage(string message) => editorOperations.ShowErrorMessageAsync(message).Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Shows a warning message to the user.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be shown.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void ShowWarningMessage(string message) => editorOperations.ShowWarningMessageAsync(message).Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the status bar message in the editor UI (if applicable).
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be shown.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void SetStatusBarMessage(string message) => editorOperations.SetStatusBarMessageAsync(message, null).Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the status bar message in the editor UI (if applicable).
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be shown.</param>
|
||||
/// <param name="timeout">A timeout in milliseconds for how long the message should remain visible.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void SetStatusBarMessage(string message, int timeout) => editorOperations.SetStatusBarMessageAsync(message, timeout).Wait();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
97
src/PowerShellEditorServices/Extensions/EditorWorkspace.cs
Normal file
97
src/PowerShellEditorServices/Extensions/EditorWorkspace.cs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a PowerShell-facing API which allows scripts to
|
||||
/// interact with the editor's workspace.
|
||||
/// </summary>
|
||||
public sealed class EditorWorkspace
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
private readonly IEditorOperations editorOperations;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server's initial working directory, since the extension API doesn't have a
|
||||
/// multi-root workspace concept.
|
||||
/// </summary>
|
||||
public string Path => editorOperations.GetWorkspacePath();
|
||||
|
||||
/// <summary>
|
||||
/// Get all the workspace folders' paths.
|
||||
/// </summary>
|
||||
public string[] Paths => editorOperations.GetWorkspacePaths();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
internal EditorWorkspace(IEditorOperations editorOperations) => this.editorOperations = editorOperations;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
// TODO: Consider returning bool instead of void to indicate success?
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new file in the editor.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void NewFile() => editorOperations.NewFileAsync(string.Empty).Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new file in the editor.
|
||||
/// </summary>
|
||||
/// <param name="content">The content to place in the new file.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void NewFile(string content) => editorOperations.NewFileAsync(content).Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Opens a file in the workspace. If the file is already open
|
||||
/// its buffer will be made active.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the file to be opened.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void OpenFile(string filePath) => editorOperations.OpenFileAsync(filePath).Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Opens a file in the workspace. If the file is already open
|
||||
/// its buffer will be made active.
|
||||
/// You can specify whether the file opens as a preview or as a durable editor.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the file to be opened.</param>
|
||||
/// <param name="preview">Determines wether the file is opened as a preview or as a durable editor.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void OpenFile(string filePath, bool preview) => editorOperations.OpenFileAsync(filePath, preview).Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Closes a file in the workspace.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the file to be closed.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void CloseFile(string filePath) => editorOperations.CloseFileAsync(filePath).Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Saves an open file in the workspace.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the file to be saved.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void SaveFile(string filePath) => editorOperations.SaveFileAsync(filePath).Wait();
|
||||
|
||||
/// <summary>
|
||||
/// Saves a file with a new name AKA a copy.
|
||||
/// </summary>
|
||||
/// <param name="oldFilePath">The file to copy.</param>
|
||||
/// <param name="newFilePath">The file to create.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void SaveFile(string oldFilePath, string newFilePath) => editorOperations.SaveFileAsync(oldFilePath, newFilePath).Wait();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
254
src/PowerShellEditorServices/Extensions/FileContext.cs
Normal file
254
src/PowerShellEditorServices/Extensions/FileContext.cs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Management.Automation.Language;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides context for a file that is open in the editor.
|
||||
/// </summary>
|
||||
public sealed class FileContext
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
private readonly ScriptFile scriptFile;
|
||||
private readonly EditorContext editorContext;
|
||||
private readonly IEditorOperations editorOperations;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parsed abstract syntax tree for the file.
|
||||
/// </summary>
|
||||
public Ast Ast => scriptFile.ScriptAst;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a BufferRange which represents the entire content
|
||||
/// range of the file.
|
||||
/// </summary>
|
||||
public IFileRange FileRange => new BufferFileRange(scriptFile.FileRange);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the language of the file.
|
||||
/// </summary>
|
||||
public string Language { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filesystem path of the file.
|
||||
/// </summary>
|
||||
public string Path => scriptFile.FilePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URI of the file.
|
||||
/// </summary>
|
||||
public Uri Uri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parsed token list for the file.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Token> Tokens => scriptFile.ScriptTokens;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the workspace-relative path of the file.
|
||||
/// </summary>
|
||||
public string WorkspacePath => editorOperations.GetWorkspaceRelativePath(scriptFile);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the FileContext class.
|
||||
/// </summary>
|
||||
/// <param name="scriptFile">The ScriptFile to which this file refers.</param>
|
||||
/// <param name="editorContext">The EditorContext to which this file relates.</param>
|
||||
/// <param name="editorOperations">An IEditorOperations implementation which performs operations in the editor.</param>
|
||||
/// <param name="language">Determines the language of the file.false If it is not specified, then it defaults to "Unknown"</param>
|
||||
internal FileContext(
|
||||
ScriptFile scriptFile,
|
||||
EditorContext editorContext,
|
||||
IEditorOperations editorOperations,
|
||||
string language = "Unknown")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(language))
|
||||
{
|
||||
language = "Unknown";
|
||||
}
|
||||
|
||||
this.scriptFile = scriptFile;
|
||||
this.editorContext = editorContext;
|
||||
this.editorOperations = editorOperations;
|
||||
Language = language;
|
||||
Uri = scriptFile.DocumentUri.ToUri();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Text Accessors
|
||||
|
||||
/// <summary>
|
||||
/// Gets the complete file content as a string.
|
||||
/// </summary>
|
||||
/// <returns>A string containing the complete file content.</returns>
|
||||
public string GetText() => scriptFile.Contents;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file content in the specified range as a string.
|
||||
/// </summary>
|
||||
/// <param name="bufferRange">The buffer range for which content will be extracted.</param>
|
||||
/// <returns>A string with the specified range of content.</returns>
|
||||
public string GetText(FileRange bufferRange)
|
||||
{
|
||||
return
|
||||
string.Join(
|
||||
Environment.NewLine,
|
||||
GetTextLines(bufferRange));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the complete file content as an array of strings.
|
||||
/// </summary>
|
||||
/// <returns>An array of strings, each representing a line in the file.</returns>
|
||||
public string[] GetTextLines() => scriptFile.FileLines.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file content in the specified range as an array of strings.
|
||||
/// </summary>
|
||||
/// <param name="fileRange">The buffer range for which content will be extracted.</param>
|
||||
/// <returns>An array of strings, each representing a line in the file within the specified range.</returns>
|
||||
public string[] GetTextLines(FileRange fileRange) => scriptFile.GetLinesInRange(fileRange.ToBufferRange());
|
||||
|
||||
#endregion
|
||||
|
||||
#region Text Manipulation
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a text string at the current cursor position represented by
|
||||
/// the parent EditorContext's CursorPosition property.
|
||||
/// </summary>
|
||||
/// <param name="textToInsert">The text string to insert.</param>
|
||||
public void InsertText(string textToInsert)
|
||||
{
|
||||
// Is there a selection?
|
||||
if (editorContext.SelectedRange.HasRange())
|
||||
{
|
||||
InsertText(
|
||||
textToInsert,
|
||||
editorContext.SelectedRange);
|
||||
}
|
||||
else
|
||||
{
|
||||
InsertText(
|
||||
textToInsert,
|
||||
editorContext.CursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a text string at the specified buffer position.
|
||||
/// </summary>
|
||||
/// <param name="textToInsert">The text string to insert.</param>
|
||||
/// <param name="insertPosition">The position at which the text will be inserted.</param>
|
||||
public void InsertText(string textToInsert, IFilePosition insertPosition)
|
||||
{
|
||||
InsertText(
|
||||
textToInsert,
|
||||
new FileRange(insertPosition, insertPosition));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a text string at the specified line and column numbers.
|
||||
/// </summary>
|
||||
/// <param name="textToInsert">The text string to insert.</param>
|
||||
/// <param name="insertLine">The 1-based line number at which the text will be inserted.</param>
|
||||
/// <param name="insertColumn">The 1-based column number at which the text will be inserted.</param>
|
||||
public void InsertText(string textToInsert, int insertLine, int insertColumn)
|
||||
{
|
||||
InsertText(
|
||||
textToInsert,
|
||||
new FilePosition(insertLine, insertColumn));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a text string to replace the specified range, represented
|
||||
/// by starting and ending line and column numbers. Can be used to
|
||||
/// insert, replace, or delete text depending on the specified range
|
||||
/// and text to insert.
|
||||
/// </summary>
|
||||
/// <param name="textToInsert">The text string to insert.</param>
|
||||
/// <param name="startLine">The 1-based starting line number where text will be replaced.</param>
|
||||
/// <param name="startColumn">The 1-based starting column number where text will be replaced.</param>
|
||||
/// <param name="endLine">The 1-based ending line number where text will be replaced.</param>
|
||||
/// <param name="endColumn">The 1-based ending column number where text will be replaced.</param>
|
||||
public void InsertText(
|
||||
string textToInsert,
|
||||
int startLine,
|
||||
int startColumn,
|
||||
int endLine,
|
||||
int endColumn)
|
||||
{
|
||||
InsertText(
|
||||
textToInsert,
|
||||
new FileRange(
|
||||
new FilePosition(startLine, startColumn),
|
||||
new FilePosition(endLine, endColumn)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a text string to replace the specified range. Can be
|
||||
/// used to insert, replace, or delete text depending on the specified
|
||||
/// range and text to insert.
|
||||
/// </summary>
|
||||
/// <param name="textToInsert">The text string to insert.</param>
|
||||
/// <param name="insertRange">The buffer range which will be replaced by the string.</param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void InsertText(string textToInsert, IFileRange insertRange)
|
||||
{
|
||||
editorOperations
|
||||
.InsertTextAsync(scriptFile.DocumentUri.ToString(), textToInsert, insertRange.ToBufferRange())
|
||||
.Wait();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Manipulation
|
||||
|
||||
/// <summary>
|
||||
/// Saves this file.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "Supporting synchronous API.")]
|
||||
public void Save() => editorOperations.SaveFileAsync(scriptFile.FilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Save this file under a new path and open a new editor window on that file.
|
||||
/// </summary>
|
||||
/// <param name="newFilePath">
|
||||
/// the path where the file should be saved,
|
||||
/// including the file name with extension as the leaf
|
||||
/// </param>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")]
|
||||
public void SaveAs(string newFilePath)
|
||||
{
|
||||
// Do some validation here so that we can provide a helpful error if the path won't work
|
||||
string absolutePath = System.IO.Path.IsPathRooted(newFilePath) ?
|
||||
newFilePath :
|
||||
System.IO.Path.GetFullPath(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(scriptFile.FilePath), newFilePath));
|
||||
|
||||
if (File.Exists(absolutePath))
|
||||
{
|
||||
throw new IOException(string.Format("The file '{0}' already exists", absolutePath));
|
||||
}
|
||||
|
||||
editorOperations.SaveFileAsync(scriptFile.FilePath, newFilePath).Wait();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
144
src/PowerShellEditorServices/Extensions/IEditorOperations.cs
Normal file
144
src/PowerShellEditorServices/Extensions/IEditorOperations.cs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an interface that must be implemented by an editor
|
||||
/// host to perform operations invoked by extensions written in
|
||||
/// PowerShell.
|
||||
/// </summary>
|
||||
internal interface IEditorOperations
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the EditorContext for the editor's current state.
|
||||
/// </summary>
|
||||
/// <returns>A new EditorContext object.</returns>
|
||||
Task<EditorContext> GetEditorContextAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server's initial working directory, since the extension API doesn't have a
|
||||
/// multi-root workspace concept.
|
||||
/// </summary>
|
||||
/// <returns>The server's initial working directory.</returns>
|
||||
string GetWorkspacePath();
|
||||
|
||||
/// <summary>
|
||||
/// Get all the workspace folders' paths.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string[] GetWorkspacePaths();
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the given file path relative to the current workspace path.
|
||||
/// </summary>
|
||||
/// <returns>The resolved file path.</returns>
|
||||
string GetWorkspaceRelativePath(ScriptFile scriptFile);
|
||||
|
||||
/// <summary>
|
||||
/// Causes a new untitled file to be created in the editor.
|
||||
/// </summary>
|
||||
/// <returns>A task that can be awaited for completion.</returns>
|
||||
Task NewFileAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Causes a new untitled file to be created in the editor.
|
||||
/// </summary>
|
||||
/// <param name="content">The content to insert into the new file.</param>
|
||||
/// <returns>A task that can be awaited for completion.</returns>
|
||||
Task NewFileAsync(string content);
|
||||
|
||||
/// <summary>
|
||||
/// Causes a file to be opened in the editor. If the file is
|
||||
/// already open, the editor must switch to the file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path of the file to be opened.</param>
|
||||
/// <returns>A Task that can be tracked for completion.</returns>
|
||||
Task OpenFileAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Causes a file to be opened in the editor. If the file is
|
||||
/// already open, the editor must switch to the file.
|
||||
/// You can specify whether the file opens as a preview or as a durable editor.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path of the file to be opened.</param>
|
||||
/// <param name="preview">Determines wether the file is opened as a preview or as a durable editor.</param>
|
||||
/// <returns>A Task that can be tracked for completion.</returns>
|
||||
Task OpenFileAsync(string filePath, bool preview);
|
||||
|
||||
/// <summary>
|
||||
/// Causes a file to be closed in the editor.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path of the file to be closed.</param>
|
||||
/// <returns>A Task that can be tracked for completion.</returns>
|
||||
Task CloseFileAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Causes a file to be saved in the editor.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path of the file to be saved.</param>
|
||||
/// <returns>A Task that can be tracked for completion.</returns>
|
||||
Task SaveFileAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Causes a file to be saved as a new file in a new editor window.
|
||||
/// </summary>
|
||||
/// <param name="oldFilePath">the path of the current file being saved</param>
|
||||
/// <param name="newFilePath">the path of the new file where the current window content will be saved</param>
|
||||
/// <returns></returns>
|
||||
Task SaveFileAsync(string oldFilePath, string newFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts text into the specified range for the file at the specified path.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path of the file which will have text inserted.</param>
|
||||
/// <param name="insertText">The text to insert into the file.</param>
|
||||
/// <param name="insertRange">The range in the file to be replaced.</param>
|
||||
/// <returns>A Task that can be tracked for completion.</returns>
|
||||
Task InsertTextAsync(string filePath, string insertText, BufferRange insertRange);
|
||||
|
||||
/// <summary>
|
||||
/// Causes the selection to be changed in the editor's active file buffer.
|
||||
/// </summary>
|
||||
/// <param name="selectionRange">The range over which the selection will be made.</param>
|
||||
/// <returns>A Task that can be tracked for completion.</returns>
|
||||
Task SetSelectionAsync(BufferRange selectionRange);
|
||||
|
||||
/// <summary>
|
||||
/// Shows an informational message to the user.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be shown.</param>
|
||||
/// <returns>A Task that can be tracked for completion.</returns>
|
||||
Task ShowInformationMessageAsync(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Shows an error message to the user.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be shown.</param>
|
||||
/// <returns>A Task that can be tracked for completion.</returns>
|
||||
Task ShowErrorMessageAsync(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a warning message to the user.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be shown.</param>
|
||||
/// <returns>A Task that can be tracked for completion.</returns>
|
||||
Task ShowWarningMessageAsync(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the status bar message in the editor UI (if applicable).
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be shown.</param>
|
||||
/// <param name="timeout">If non-null, a timeout in milliseconds for how long the message should remain visible.</param>
|
||||
/// <returns>A Task that can be tracked for completion.</returns>
|
||||
Task SetStatusBarMessageAsync(string message, int? timeout);
|
||||
|
||||
/// <summary>
|
||||
/// Triggers to the editor to clear the terminal.
|
||||
/// </summary>
|
||||
void ClearTerminal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation-free class designed to safely allow PowerShell Editor Services to be loaded in an obvious way.
|
||||
/// Referencing this class will force looking for and loading the PSES assembly if it's not already loaded.
|
||||
/// </summary>
|
||||
internal static class EditorServicesLoading
|
||||
{
|
||||
internal static void LoadEditorServicesForHost()
|
||||
{
|
||||
// No-op that forces loading this assembly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.PowerShell.EditorServices.Server;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Extension;
|
||||
|
||||
// The HostLogger type isn't directly referenced from this assembly, however it uses a common IObservable interface and this alias helps make it more clear the purpose. We can use Microsoft.Extensions.Logging from this point because the ALC should be loaded, but we need to only expose the IObservable to the Hosting assembly so it doesn't try to load MEL before the ALC is ready.
|
||||
using HostLogger = System.IObservable<(int logLevel, string message)>;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for creating the LSP server and debug server instances.
|
||||
/// </summary>
|
||||
internal sealed class EditorServicesServerFactory : IDisposable
|
||||
{
|
||||
private readonly HostLogger _hostLogger;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a loggerfactory for this instance
|
||||
/// </summary>
|
||||
/// <param name="hostLogger">The hostLogger that will be provided to the language services for logging handoff</param>
|
||||
internal EditorServicesServerFactory(HostLogger hostLogger) => _hostLogger = hostLogger;
|
||||
|
||||
/// <summary>
|
||||
/// Create the LSP server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only called once and that's in <see cref="Hosting.EditorServicesRunner"/>.
|
||||
/// </remarks>
|
||||
/// <param name="inputStream">The protocol transport input stream.</param>
|
||||
/// <param name="outputStream">The protocol transport output stream.</param>
|
||||
/// <param name="hostStartupInfo">The host details configuration for Editor Services
|
||||
/// instantiation.</param>
|
||||
/// <returns>A new, unstarted language server instance.</returns>
|
||||
public PsesLanguageServer CreateLanguageServer(
|
||||
Stream inputStream,
|
||||
Stream outputStream,
|
||||
HostStartupInfo hostStartupInfo) => new(_hostLogger, inputStream, outputStream, hostStartupInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Create the debug server given a language server instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only called once and that's in <see cref="Hosting.EditorServicesRunner"/>.
|
||||
/// </remarks>
|
||||
/// <param name="inputStream">The protocol transport input stream.</param>
|
||||
/// <param name="outputStream">The protocol transport output stream.</param>
|
||||
/// <param name="languageServer"></param>
|
||||
/// <returns>A new, unstarted debug server instance.</returns>
|
||||
public PsesDebugServer CreateDebugServerWithLanguageServer(
|
||||
Stream inputStream,
|
||||
Stream outputStream,
|
||||
PsesLanguageServer languageServer)
|
||||
{
|
||||
return new PsesDebugServer(
|
||||
_hostLogger,
|
||||
inputStream,
|
||||
outputStream,
|
||||
languageServer.LanguageServer.Services);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new debug server based on an old one in an ended session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only called once and that's in <see cref="Hosting.EditorServicesRunner"/>.
|
||||
/// </remarks>
|
||||
/// <param name="inputStream">The protocol transport input stream.</param>
|
||||
/// <param name="outputStream">The protocol transport output stream.</param>
|
||||
/// <param name="debugServer">The old debug server to recreate.</param>
|
||||
/// <returns></returns>
|
||||
public PsesDebugServer RecreateDebugServer(
|
||||
Stream inputStream,
|
||||
Stream outputStream,
|
||||
PsesDebugServer debugServer)
|
||||
{
|
||||
return new PsesDebugServer(
|
||||
_hostLogger,
|
||||
inputStream,
|
||||
outputStream,
|
||||
debugServer.ServiceProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a standalone debug server for temp sessions.
|
||||
/// </summary>
|
||||
/// <param name="inputStream">The protocol transport input stream.</param>
|
||||
/// <param name="outputStream">The protocol transport output stream.</param>
|
||||
/// <param name="hostStartupInfo">The host startup configuration to create the server session with.</param>
|
||||
/// <returns></returns>
|
||||
public PsesDebugServer CreateDebugServerForTempSession(
|
||||
Stream inputStream,
|
||||
Stream outputStream,
|
||||
HostStartupInfo hostStartupInfo)
|
||||
{
|
||||
ServiceProvider serviceProvider = new ServiceCollection()
|
||||
.AddLogging(builder => builder
|
||||
.ClearProviders()
|
||||
.SetMinimumLevel(LogLevel.Trace)) // TODO: Why randomly set to trace?
|
||||
.AddSingleton<ILanguageServerFacade>(_ => null)
|
||||
// TODO: Why add these for a debug server?!
|
||||
.AddPsesLanguageServices(hostStartupInfo)
|
||||
// For a Temp session, there is no LanguageServer so just set it to null
|
||||
.AddSingleton(
|
||||
typeof(ILanguageServerFacade),
|
||||
_ => null)
|
||||
.BuildServiceProvider();
|
||||
|
||||
// This gets the ExtensionService which triggers the creation of the `$psEditor` variable.
|
||||
// (because services are created only when they are first retrieved)
|
||||
// Keep in mind, for Temp sessions, the `$psEditor` API is a no-op and the user is warned
|
||||
// to run the command in the main extension terminal.
|
||||
serviceProvider.GetService<ExtensionService>();
|
||||
|
||||
return new PsesDebugServer(
|
||||
_hostLogger,
|
||||
inputStream,
|
||||
outputStream,
|
||||
serviceProvider,
|
||||
isTemp: true);
|
||||
}
|
||||
|
||||
// TODO: Clean up host logger? Shouldn't matter since we start a new process after shutdown.
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
221
src/PowerShellEditorServices/Hosting/HostStartupInfo.cs
Normal file
221
src/PowerShellEditorServices/Hosting/HostStartupInfo.cs
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Management.Automation.Host;
|
||||
using System.Management.Automation.Runspaces;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains details about the host as well as any other information needed by Editor Services
|
||||
/// at startup time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO: Simplify this as a <see langword="record"/>.
|
||||
/// </remarks>
|
||||
public sealed class HostStartupInfo
|
||||
{
|
||||
#region Constants
|
||||
|
||||
/// <summary>
|
||||
/// The default host name for PowerShell Editor Services. Used
|
||||
/// if no host name is specified by the host application.
|
||||
/// </summary>
|
||||
private const string DefaultHostName = "PowerShell Editor Services Host";
|
||||
|
||||
/// <summary>
|
||||
/// The default host ID for PowerShell Editor Services. Used
|
||||
/// for the host-specific profile path if no host ID is specified.
|
||||
/// </summary>
|
||||
private const string DefaultHostProfileId = "Microsoft.PowerShellEditorServices";
|
||||
|
||||
/// <summary>
|
||||
/// The default host version for PowerShell Editor Services. If
|
||||
/// no version is specified by the host application, we use 0.0.0
|
||||
/// to indicate a lack of version.
|
||||
/// </summary>
|
||||
private static readonly Version s_defaultHostVersion = new(0, 0, 0);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the host.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the profile ID of the host, used to determine the
|
||||
/// host-specific profile path.
|
||||
/// </summary>
|
||||
public string ProfileId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version of the host.
|
||||
/// </summary>
|
||||
public Version Version { get; }
|
||||
|
||||
public ProfilePathInfo ProfilePaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Any feature flags enabled at startup.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> FeatureFlags { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Names or paths of any additional modules to import on startup.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> AdditionalModules { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the Extension Terminal is to be enabled.
|
||||
/// </summary>
|
||||
public bool ConsoleReplEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True if we want to suppress messages to PSHost (to prevent Stdio clobbering)
|
||||
/// </summary>
|
||||
public bool UseNullPSHostUI { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the legacy PSES readline implementation will be used. Otherwise PSReadLine will be used.
|
||||
/// If the console REPL is not enabled, this setting will be ignored.
|
||||
/// </summary>
|
||||
public bool UsesLegacyReadLine { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The PowerShell host to use with Editor Services.
|
||||
/// </summary>
|
||||
public PSHost PSHost { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of the log file Editor Services should log to.
|
||||
/// </summary>
|
||||
public string LogPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The InitialSessionState will be inherited from the original PowerShell process. This will
|
||||
/// be used when creating runspaces so that we honor the same InitialSessionState.
|
||||
/// </summary>
|
||||
public InitialSessionState InitialSessionState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum log level of log events to be logged.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This primitive maps to <see cref="Hosting.PsesLogLevel"/> and <see
|
||||
/// cref="Microsoft.Extensions.Logging.LogLevel"/>
|
||||
/// </remarks>
|
||||
public int LogLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to find the bundled modules. User configurable for advanced usage.
|
||||
/// </summary>
|
||||
public string BundledModulePath { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the HostDetails class.
|
||||
/// </summary>
|
||||
/// <param name="name">
|
||||
/// The display name for the host, typically in the form of
|
||||
/// "[Application Name] Host".
|
||||
/// </param>
|
||||
/// <param name="profileId">
|
||||
/// The identifier of the PowerShell host to use for its profile path.
|
||||
/// loaded. Used to resolve a profile path of the form 'X_profile.ps1'
|
||||
/// where 'X' represents the value of hostProfileId. If null, a default
|
||||
/// will be used.
|
||||
/// </param>
|
||||
/// <param name="version">The host application's version.</param>
|
||||
/// <param name="psHost">The PowerShell host to use.</param>
|
||||
/// <param name="profilePaths">The set of profile paths.</param>
|
||||
/// <param name="featureFlags">Flags of features to enable.</param>
|
||||
/// <param name="additionalModules">Names or paths of additional modules to import.</param>
|
||||
/// <param name="initialSessionState">The language mode inherited from the orginal PowerShell process. This will be used when creating runspaces so that we honor the same initialSessionState including allowed modules, cmdlets and language mode.</param>
|
||||
/// <param name="logPath">The path to log to.</param>
|
||||
/// <param name="logLevel">The minimum log event level.</param>
|
||||
/// <param name="consoleReplEnabled">Enable console if true.</param>
|
||||
/// <param name="useNullPSHostUI">Whether or not to use the Null UI.</param>
|
||||
/// <param name="usesLegacyReadLine">Use PSReadLine if false, otherwise use the legacy readline implementation.</param>
|
||||
/// <param name="bundledModulePath">A custom path to the expected bundled modules.</param>
|
||||
public HostStartupInfo(
|
||||
string name,
|
||||
string profileId,
|
||||
Version version,
|
||||
PSHost psHost,
|
||||
ProfilePathInfo profilePaths,
|
||||
IReadOnlyList<string> featureFlags,
|
||||
IReadOnlyList<string> additionalModules,
|
||||
InitialSessionState initialSessionState,
|
||||
string logPath,
|
||||
int logLevel,
|
||||
bool consoleReplEnabled,
|
||||
bool useNullPSHostUI,
|
||||
bool usesLegacyReadLine,
|
||||
string bundledModulePath)
|
||||
{
|
||||
Name = name ?? DefaultHostName;
|
||||
ProfileId = profileId ?? DefaultHostProfileId;
|
||||
Version = version ?? s_defaultHostVersion;
|
||||
PSHost = psHost;
|
||||
ProfilePaths = profilePaths;
|
||||
FeatureFlags = featureFlags ?? Array.Empty<string>();
|
||||
AdditionalModules = additionalModules ?? Array.Empty<string>();
|
||||
InitialSessionState = initialSessionState;
|
||||
LogPath = logPath;
|
||||
LogLevel = logLevel;
|
||||
ConsoleReplEnabled = consoleReplEnabled;
|
||||
UseNullPSHostUI = useNullPSHostUI;
|
||||
UsesLegacyReadLine = usesLegacyReadLine;
|
||||
|
||||
// Respect a user provided bundled module path.
|
||||
BundledModulePath = Directory.Exists(bundledModulePath)
|
||||
? bundledModulePath
|
||||
: Path.GetFullPath(Path.Combine(
|
||||
Path.GetDirectoryName(typeof(HostStartupInfo).Assembly.Location),
|
||||
"..",
|
||||
"..",
|
||||
".."));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a strange class that is generally <c>null</c> or otherwise just has a single path
|
||||
/// set. It is eventually parsed one-by-one when setting up the PowerShell runspace.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO: Simplify this as a <see langword="record"/>.
|
||||
/// </remarks>
|
||||
public sealed class ProfilePathInfo
|
||||
{
|
||||
public ProfilePathInfo(
|
||||
string currentUserAllHosts,
|
||||
string currentUserCurrentHost,
|
||||
string allUsersAllHosts,
|
||||
string allUsersCurrentHost)
|
||||
{
|
||||
CurrentUserAllHosts = currentUserAllHosts;
|
||||
CurrentUserCurrentHost = currentUserCurrentHost;
|
||||
AllUsersAllHosts = allUsersAllHosts;
|
||||
AllUsersCurrentHost = allUsersCurrentHost;
|
||||
}
|
||||
|
||||
public string CurrentUserAllHosts { get; }
|
||||
|
||||
public string CurrentUserCurrentHost { get; }
|
||||
|
||||
public string AllUsersAllHosts { get; }
|
||||
|
||||
public string AllUsersCurrentHost { get; }
|
||||
}
|
||||
}
|
||||
21
src/PowerShellEditorServices/IsExternalInit.cs
Normal file
21
src/PowerShellEditorServices/IsExternalInit.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
|
||||
#else
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Reserved to be used by the compiler for tracking metadata.
|
||||
/// This class should not be used by developers in source code.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal static class IsExternalInit
|
||||
{
|
||||
}
|
||||
}
|
||||
#endif
|
||||
26
src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs
Normal file
26
src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Adapter class to allow logging events sent by the host to be recorded by PSES' logging infrastructure.
|
||||
/// </summary>
|
||||
internal class HostLoggerAdapter(ILogger logger) : IObserver<(int logLevel, string message)>
|
||||
{
|
||||
public void OnError(Exception error) => logger.LogError(error, "Error in host logger");
|
||||
|
||||
/// <summary>
|
||||
/// Log the message received from the host into MEL.
|
||||
/// </summary>
|
||||
public void OnNext((int logLevel, string message) value) => logger.Log((LogLevel)value.logLevel, value.message);
|
||||
|
||||
public void OnCompleted()
|
||||
{
|
||||
// Nothing to do; we simply don't send more log messages
|
||||
}
|
||||
}
|
||||
}
|
||||
184
src/PowerShellEditorServices/Logging/LanguageServerLogger.cs
Normal file
184
src/PowerShellEditorServices/Logging/LanguageServerLogger.cs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Window;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Logging;
|
||||
|
||||
internal class LanguageServerLogger(ILanguageServerFacade responseRouter, string categoryName) : ILogger
|
||||
{
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => Disposable.Empty;
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter
|
||||
)
|
||||
{
|
||||
if (responseRouter is null)
|
||||
{
|
||||
throw new InvalidOperationException("Log received without a valid responseRouter dependency. This is a bug, please report it.");
|
||||
}
|
||||
// Any Omnisharp or trace logs are directly LSP protocol related and we send them to the trace channel
|
||||
// TODO: Dynamically adjust if SetTrace is reported
|
||||
// BUG: There is an omnisharp filter incorrectly filtering this. As a workaround we will use logMessage for now.
|
||||
// https://github.com/OmniSharp/csharp-language-server-protocol/issues/1390
|
||||
//
|
||||
// {
|
||||
// // Everything with omnisharp goes directly to trace
|
||||
// string eventMessage = string.Empty;
|
||||
// string exceptionName = exception?.GetType().Name ?? string.Empty;
|
||||
// if (eventId.Name is not null)
|
||||
// {
|
||||
// eventMessage = eventId.Id == 0 ? eventId.Name : $"{eventId.Name} [{eventId.Id}] ";
|
||||
// }
|
||||
|
||||
// LogTraceParams trace = new()
|
||||
// {
|
||||
// Message = categoryName + ": " + eventMessage + exceptionName,
|
||||
// Verbose = formatter(state, exception)
|
||||
// };
|
||||
// responseRouter.Client.LogTrace(trace);
|
||||
// }
|
||||
|
||||
// Drop all omnisharp messages to trace. This isn't a MEL filter because it's specific only to this provider.
|
||||
if (categoryName.StartsWith("OmniSharp.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logLevel = LogLevel.Trace;
|
||||
}
|
||||
|
||||
(MessageType messageType, string messagePrepend) = GetMessageInfo(logLevel);
|
||||
|
||||
// The vscode-languageserver-node client doesn't support LogOutputChannel as of 2024-11-24 and also doesn't
|
||||
// provide a way to middleware the incoming log messages, so our output channel has no idea what the logLevel
|
||||
// is. As a workaround, we send the severity in-line with the message for the client to parse.
|
||||
// BUG: https://github.com/microsoft/vscode-languageserver-node/issues/1116
|
||||
if (responseRouter.Client?.ClientSettings?.ClientInfo?.Name == "Visual Studio Code")
|
||||
{
|
||||
messagePrepend = logLevel switch
|
||||
{
|
||||
LogLevel.Critical => "<Error>CRITICAL: ",
|
||||
LogLevel.Error => "<Error>",
|
||||
LogLevel.Warning => "<Warning>",
|
||||
LogLevel.Information => "<Info>",
|
||||
LogLevel.Debug => "<Debug>",
|
||||
LogLevel.Trace => "<Trace>",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
// The vscode formatter prepends some extra stuff to Info specifically, so we drop Info to Log, but it will get logged correctly on the other side thanks to our inline indicator that our custom parser on the other side will pick up and process.
|
||||
if (messageType == MessageType.Info)
|
||||
{
|
||||
messageType = MessageType.Log;
|
||||
}
|
||||
}
|
||||
|
||||
LogMessageParams logMessage = new()
|
||||
{
|
||||
Type = messageType,
|
||||
Message = messagePrepend + categoryName + ": " + formatter(state, exception) +
|
||||
(exception != null ? " - " + exception : "") + " | " +
|
||||
//Hopefully this isn't too expensive in the long run
|
||||
FormatState(state, exception)
|
||||
};
|
||||
responseRouter.Window.Log(logMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats the state object into a string for logging.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is copied from Omnisharp, we can probably do better.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TState"></typeparam>
|
||||
/// <param name="state"></param>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
private static string FormatState<TState>(TState state, Exception? exception)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
IEnumerable<KeyValuePair<string, object>> dict => string.Join(" ", dict.Where(z => z.Key != "{OriginalFormat}").Select(z => $"{z.Key}='{z.Value}'")),
|
||||
_ => JsonConvert.SerializeObject(state).Replace("\"", "'")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps MEL log levels to LSP message types
|
||||
/// </summary>
|
||||
private static (MessageType messageType, string messagePrepend) GetMessageInfo(LogLevel logLevel)
|
||||
=> logLevel switch
|
||||
{
|
||||
LogLevel.Critical => (MessageType.Error, "Critical: "),
|
||||
LogLevel.Error => (MessageType.Error, string.Empty),
|
||||
LogLevel.Warning => (MessageType.Warning, string.Empty),
|
||||
LogLevel.Information => (MessageType.Info, string.Empty),
|
||||
LogLevel.Debug => (MessageType.Log, string.Empty),
|
||||
LogLevel.Trace => (MessageType.Log, "Trace: "),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null)
|
||||
};
|
||||
}
|
||||
|
||||
internal class LanguageServerLoggerProvider(ILanguageServerFacade languageServer) : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string categoryName) => new LanguageServerLogger(languageServer, categoryName);
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
public static class LanguageServerLoggerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a custom logger provider for PSES LSP, that provides more granular categorization than the default Omnisharp logger, such as separating Omnisharp and PSES messages to different channels.
|
||||
/// </summary>
|
||||
public static ILoggingBuilder AddPsesLanguageServerLogging(this ILoggingBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<ILoggerProvider, LanguageServerLoggerProvider>();
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static ILoggingBuilder AddLspClientConfigurableMinimumLevel(
|
||||
this ILoggingBuilder builder,
|
||||
LogLevel initialLevel = LogLevel.Trace
|
||||
)
|
||||
{
|
||||
builder.Services.AddOptions<LoggerFilterOptions>();
|
||||
builder.Services.AddSingleton<DynamicLogLevelOptions>(sp =>
|
||||
{
|
||||
IOptionsMonitor<LoggerFilterOptions> optionsMonitor = sp.GetRequiredService<IOptionsMonitor<LoggerFilterOptions>>();
|
||||
return new(initialLevel, optionsMonitor);
|
||||
});
|
||||
builder.Services.AddSingleton<IConfigureOptions<LoggerFilterOptions>>(sp =>
|
||||
sp.GetRequiredService<DynamicLogLevelOptions>());
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
internal class DynamicLogLevelOptions(
|
||||
LogLevel initialLevel,
|
||||
IOptionsMonitor<LoggerFilterOptions> optionsMonitor) : IConfigureOptions<LoggerFilterOptions>
|
||||
{
|
||||
private LogLevel _currentLevel = initialLevel;
|
||||
private readonly IOptionsMonitor<LoggerFilterOptions> _optionsMonitor = optionsMonitor;
|
||||
|
||||
public void Configure(LoggerFilterOptions options) => options.MinLevel = _currentLevel;
|
||||
|
||||
public void SetLogLevel(LogLevel level)
|
||||
{
|
||||
_currentLevel = level;
|
||||
// Trigger reload of options to apply new log level
|
||||
_optionsMonitor.CurrentValue.MinLevel = level;
|
||||
}
|
||||
}
|
||||
29
src/PowerShellEditorServices/Logging/LoggerExtensions.cs
Normal file
29
src/PowerShellEditorServices/Logging/LoggerExtensions.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Logging
|
||||
{
|
||||
internal static class LoggerExtensions
|
||||
{
|
||||
// TODO: These need to be fixed (and used consistently).
|
||||
public static void LogException(
|
||||
this ILogger logger,
|
||||
string message,
|
||||
Exception exception,
|
||||
[CallerMemberName] string callerName = null,
|
||||
[CallerFilePath] string callerSourceFile = null,
|
||||
[CallerLineNumber] int callerLineNumber = -1) => logger.LogError(message, exception);
|
||||
|
||||
public static void LogHandledException(
|
||||
this ILogger logger,
|
||||
string message,
|
||||
Exception exception,
|
||||
[CallerMemberName] string callerName = null,
|
||||
[CallerFilePath] string callerSourceFile = null,
|
||||
[CallerLineNumber] int callerLineNumber = -1) => logger.LogWarning(message, exception);
|
||||
}
|
||||
}
|
||||
27
src/PowerShellEditorServices/Logging/PsesTelemetryEvent.cs
Normal file
27
src/PowerShellEditorServices/Logging/PsesTelemetryEvent.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Logging
|
||||
{
|
||||
// This inherits from Dictionary so that it can be passed in to SendTelemetryEvent()
|
||||
// which takes in an IDictionary<string, object>
|
||||
// However, I wanted creation to be easy so you can do
|
||||
// new PsesTelemetryEvent { EventName = "eventName", Data = data }
|
||||
internal class PsesTelemetryEvent : Dictionary<string, object>
|
||||
{
|
||||
public string EventName
|
||||
{
|
||||
get => this["EventName"].ToString() ?? "PsesEvent";
|
||||
set => this["EventName"] = value;
|
||||
}
|
||||
|
||||
public JObject Data
|
||||
{
|
||||
get => this["Data"] as JObject ?? new JObject();
|
||||
set => this["Data"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/PowerShellEditorServices/PowerShellEditorServices.csproj
Normal file
65
src/PowerShellEditorServices/PowerShellEditorServices.csproj
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import
|
||||
Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), PowerShellEditorServices.Common.props))\PowerShellEditorServices.Common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>PowerShell Editor Services</AssemblyTitle>
|
||||
<Description>Provides common PowerShell editor capabilities as a .NET library.</Description>
|
||||
<TargetFrameworks>netstandard2.0</TargetFrameworks>
|
||||
<AssemblyName>Microsoft.PowerShell.EditorServices</AssemblyName>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Be careful about using CoreCLR as a definition, it doesn't work for most of
|
||||
our code because the shared libraries target netstandard2.0 and so can't use
|
||||
a property group condition to define it. It's only available to code under
|
||||
src/PowerShellEditorServices.Hosting and the tests.
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Microsoft.PowerShell.EditorServices.Hosting</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Microsoft.PowerShell.EditorServices.Test</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>PowerShellEditorServices.Test.E2E</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Microsoft.PowerShell.EditorServices.Test.Shared</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="PowerShellStandard.Library" />
|
||||
<PackageReference Include="System.IO.Pipes.AccessControl" />
|
||||
<PackageReference Include="System.Security.Principal" />
|
||||
<PackageReference Include="System.Security.Principal.Windows" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Roslynator">
|
||||
<PackageReference Include="Roslynator.Analyzers" />
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" />
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" />
|
||||
</ItemGroup>
|
||||
|
||||
<Choose>
|
||||
<When Condition=" '$(LocalOmniSharp)' == 'true' ">
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\csharp-language-server-protocol\src\Server\Server.csproj" />
|
||||
<ProjectReference Include="..\..\..\csharp-language-server-protocol\src\Dap.Server\Dap.Server.csproj" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OmniSharp.Extensions.LanguageServer" />
|
||||
<PackageReference Include="OmniSharp.Extensions.DebugAdapter.Server" />
|
||||
</ItemGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
</Project>
|
||||
161
src/PowerShellEditorServices/Server/PsesDebugServer.cs
Normal file
161
src/PowerShellEditorServices/Server/PsesDebugServer.cs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerShell.EditorServices.Handlers;
|
||||
using Microsoft.PowerShell.EditorServices.Services;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
|
||||
using OmniSharp.Extensions.DebugAdapter.Server;
|
||||
using OmniSharp.Extensions.LanguageServer.Server;
|
||||
|
||||
// See EditorServicesServerFactory.cs for the explanation of this alias.
|
||||
using HostLogger = System.IObservable<(int logLevel, string message)>;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server for hosting debug sessions.
|
||||
/// </summary>
|
||||
internal class PsesDebugServer : IDisposable
|
||||
{
|
||||
private readonly Stream _inputStream;
|
||||
private readonly Stream _outputStream;
|
||||
private readonly TaskCompletionSource<bool> _serverStopped;
|
||||
private DebugAdapterServer _debugAdapterServer;
|
||||
private PsesInternalHost _psesHost;
|
||||
private bool _startedPses;
|
||||
private readonly bool _isTemp;
|
||||
// FIXME: This was never actually used in the debug server. Since we never have a debug server without an LSP, we could probably remove this and either reuse the MEL from the LSP, or create a new one here. It is probably best to only use this for exceptions that we can't reasonably send via the DAP protocol, which should only be anything before the initialize request.
|
||||
protected readonly HostLogger _hostLogger;
|
||||
|
||||
public PsesDebugServer(
|
||||
HostLogger hostLogger,
|
||||
Stream inputStream,
|
||||
Stream outputStream,
|
||||
IServiceProvider serviceProvider,
|
||||
bool isTemp = false)
|
||||
{
|
||||
_hostLogger = hostLogger;
|
||||
_inputStream = inputStream;
|
||||
_outputStream = outputStream;
|
||||
ServiceProvider = serviceProvider;
|
||||
_isTemp = isTemp;
|
||||
_serverStopped = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
internal IServiceProvider ServiceProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Start the debug server listening.
|
||||
/// </summary>
|
||||
/// <returns>A task that completes when the server is ready.</returns>
|
||||
public async Task StartAsync()
|
||||
{
|
||||
_debugAdapterServer = await DebugAdapterServer.From(options =>
|
||||
{
|
||||
// We need to let the PowerShell Context Service know that we are in a debug session
|
||||
// so that it doesn't send the powerShell/startDebugger message.
|
||||
_psesHost = ServiceProvider.GetService<PsesInternalHost>();
|
||||
_psesHost.DebugContext.IsDebugServerActive = true;
|
||||
|
||||
options
|
||||
.WithInput(_inputStream)
|
||||
.WithOutput(_outputStream)
|
||||
.WithServices(serviceCollection =>
|
||||
serviceCollection
|
||||
.AddOptions()
|
||||
.AddPsesDebugServices(ServiceProvider, this))
|
||||
// TODO: Consider replacing all WithHandler with AddSingleton
|
||||
.WithHandler<LaunchAndAttachHandler>()
|
||||
.WithHandler<DisconnectHandler>()
|
||||
.WithHandler<BreakpointHandlers>()
|
||||
.WithHandler<ConfigurationDoneHandler>()
|
||||
.WithHandler<ThreadsHandler>()
|
||||
.WithHandler<StackTraceHandler>()
|
||||
.WithHandler<ScopesHandler>()
|
||||
.WithHandler<VariablesHandler>()
|
||||
.WithHandler<ContinueHandler>()
|
||||
.WithHandler<NextHandler>()
|
||||
.WithHandler<PauseHandler>()
|
||||
.WithHandler<StepInHandler>()
|
||||
.WithHandler<StepOutHandler>()
|
||||
.WithHandler<SourceHandler>()
|
||||
.WithHandler<SetVariableHandler>()
|
||||
.WithHandler<DebugEvaluateHandler>()
|
||||
// The OnInitialize delegate gets run when we first receive the _Initialize_ request:
|
||||
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
|
||||
.OnInitialize(async (server, _, cancellationToken) =>
|
||||
{
|
||||
// Start the host if not already started, and enable debug mode (required
|
||||
// for remote debugging).
|
||||
//
|
||||
// TODO: We might need to fill in HostStartOptions here.
|
||||
_startedPses = !await _psesHost.TryStartAsync(new HostStartOptions(), cancellationToken).ConfigureAwait(false);
|
||||
_psesHost.DebugContext.EnableDebugMode();
|
||||
|
||||
// We need to give the host a handle to the DAP so it can register
|
||||
// notifications (specifically for sendKeyPress).
|
||||
if (_isTemp)
|
||||
{
|
||||
_psesHost.DebugServer = server;
|
||||
}
|
||||
|
||||
// Clear any existing breakpoints before proceeding.
|
||||
BreakpointService breakpointService = server.GetService<BreakpointService>();
|
||||
await breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false);
|
||||
})
|
||||
// The OnInitialized delegate gets run right before the server responds to the _Initialize_ request:
|
||||
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
|
||||
.OnInitialized((_, _, response, _) =>
|
||||
{
|
||||
response.SupportsConditionalBreakpoints = true;
|
||||
response.SupportsConfigurationDoneRequest = true;
|
||||
response.SupportsFunctionBreakpoints = true;
|
||||
response.SupportsHitConditionalBreakpoints = true;
|
||||
response.SupportsLogPoints = true;
|
||||
response.SupportsSetVariable = true;
|
||||
response.SupportsDelayedStackTraceLoading = true;
|
||||
|
||||
return Task.CompletedTask;
|
||||
})
|
||||
;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Note that the lifetime of the DebugContext is longer than the debug server;
|
||||
// It represents the debugger on the PowerShell process we're in,
|
||||
// while a new debug server is spun up for every debugging session
|
||||
_psesHost.DebugContext.IsDebugServerActive = false;
|
||||
_debugAdapterServer?.Dispose();
|
||||
_inputStream.Dispose();
|
||||
_outputStream.Dispose();
|
||||
_serverStopped.SetResult(true);
|
||||
// TODO: If the debugger has stopped, should we clear the breakpoints?
|
||||
}
|
||||
|
||||
public async Task WaitForShutdown()
|
||||
{
|
||||
await _serverStopped.Task.ConfigureAwait(false);
|
||||
|
||||
// If we started the host, we need to ensure any errors are marshalled back to us like this
|
||||
if (_startedPses)
|
||||
{
|
||||
_psesHost.TriggerShutdown();
|
||||
await _psesHost.Shutdown.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler SessionEnded;
|
||||
|
||||
internal void OnSessionEnded() => SessionEnded?.Invoke(this, null);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
193
src/PowerShellEditorServices/Server/PsesLanguageServer.cs
Normal file
193
src/PowerShellEditorServices/Server/PsesLanguageServer.cs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.PowerShell.EditorServices.Handlers;
|
||||
using Microsoft.PowerShell.EditorServices.Hosting;
|
||||
using Microsoft.PowerShell.EditorServices.Logging;
|
||||
using Microsoft.PowerShell.EditorServices.Services;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Extension;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OmniSharp.Extensions.JsonRpc;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.General;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
|
||||
using OmniSharp.Extensions.LanguageServer.Server;
|
||||
|
||||
// See EditorServicesServerFactory.cs for the explanation of this alias.
|
||||
using HostLogger = System.IObservable<(int logLevel, string message)>;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server runner class for handling LSP messages for Editor Services.
|
||||
/// </summary>
|
||||
internal class PsesLanguageServer
|
||||
{
|
||||
internal HostLogger HostLogger { get; }
|
||||
internal ILanguageServer LanguageServer { get; private set; }
|
||||
private readonly LogLevel _minimumLogLevel;
|
||||
private readonly Stream _inputStream;
|
||||
private readonly Stream _outputStream;
|
||||
private readonly HostStartupInfo _hostDetails;
|
||||
private readonly TaskCompletionSource<bool> _serverStart;
|
||||
private PsesInternalHost _psesHost;
|
||||
private IDisposable hostLoggerSubscription;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new language server instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is only ever instantiated via <see
|
||||
/// cref="EditorServicesServerFactory.CreateLanguageServer"/>. It is essentially a
|
||||
/// singleton. The factory hides the logger.
|
||||
/// </remarks>
|
||||
/// <param name="hostLogger">The host logger to hand off for monitoring.</param>
|
||||
/// <param name="inputStream">Protocol transport input stream.</param>
|
||||
/// <param name="outputStream">Protocol transport output stream.</param>
|
||||
/// <param name="hostStartupInfo">Host configuration to instantiate the server and services
|
||||
/// with.</param>
|
||||
public PsesLanguageServer(
|
||||
HostLogger hostLogger,
|
||||
Stream inputStream,
|
||||
Stream outputStream,
|
||||
HostStartupInfo hostStartupInfo)
|
||||
{
|
||||
HostLogger = hostLogger;
|
||||
_minimumLogLevel = (LogLevel)hostStartupInfo.LogLevel;
|
||||
_inputStream = inputStream;
|
||||
_outputStream = outputStream;
|
||||
_hostDetails = hostStartupInfo;
|
||||
_serverStart = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the server listening for input.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For the services (including the <see cref="PowerShellContextService">
|
||||
/// context wrapper around PowerShell itself) see <see
|
||||
/// cref="PsesServiceCollectionExtensions.AddPsesLanguageServices"/>.
|
||||
/// </remarks>
|
||||
/// <returns>A task that completes when the server is ready and listening.</returns>
|
||||
public async Task StartAsync()
|
||||
{
|
||||
LanguageServer = await OmniSharp.Extensions.LanguageServer.Server.LanguageServer.From(options =>
|
||||
{
|
||||
options
|
||||
.WithInput(_inputStream)
|
||||
.WithOutput(_outputStream)
|
||||
.WithServices(serviceCollection =>
|
||||
{
|
||||
// NOTE: This adds a lot of services!
|
||||
serviceCollection.AddPsesLanguageServices(_hostDetails);
|
||||
})
|
||||
.ConfigureLogging(builder => builder
|
||||
.ClearProviders()
|
||||
.AddPsesLanguageServerLogging()
|
||||
.SetMinimumLevel(_minimumLogLevel))
|
||||
.WithHandler<PsesWorkspaceSymbolsHandler>()
|
||||
.WithHandler<PsesTextDocumentHandler>()
|
||||
.WithHandler<GetVersionHandler>()
|
||||
.WithHandler<PsesConfigurationHandler>()
|
||||
.WithHandler<PsesFoldingRangeHandler>()
|
||||
.WithHandler<PsesDocumentFormattingHandler>()
|
||||
.WithHandler<PsesDocumentRangeFormattingHandler>()
|
||||
.WithHandler<PsesReferencesHandler>()
|
||||
.WithHandler<PsesDocumentSymbolHandler>()
|
||||
.WithHandler<PsesDocumentHighlightHandler>()
|
||||
.WithHandler<PSHostProcessAndRunspaceHandlers>()
|
||||
.WithHandler<PsesCodeLensHandlers>()
|
||||
.WithHandler<PsesCodeActionHandler>()
|
||||
.WithHandler<InvokeExtensionCommandHandler>()
|
||||
// If PsesCompletionHandler is not marked as serial, then DidChangeTextDocument
|
||||
// notifications will end up cancelling completion. So quickly typing `Get-`
|
||||
// would result in no completions.
|
||||
//
|
||||
// This also lets completion requests interrupt time consuming background tasks
|
||||
// like the references code lens.
|
||||
.WithHandler<PsesCompletionHandler>(
|
||||
new JsonRpcHandlerOptions() { RequestProcessType = RequestProcessType.Serial })
|
||||
.WithHandler<PsesHoverHandler>()
|
||||
.WithHandler<PsesSignatureHelpHandler>()
|
||||
.WithHandler<PsesDefinitionHandler>()
|
||||
.WithHandler<GetCommentHelpHandler>()
|
||||
.WithHandler<EvaluateHandler>()
|
||||
.WithHandler<GetCommandHandler>()
|
||||
.WithHandler<ShowHelpHandler>()
|
||||
.WithHandler<ExpandAliasHandler>()
|
||||
.WithHandler<PsesSemanticTokensHandler>()
|
||||
.WithHandler<DidChangeWatchedFilesHandler>()
|
||||
// NOTE: The OnInitialize delegate gets run when we first receive the
|
||||
// _Initialize_ request:
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize
|
||||
.OnInitialize(
|
||||
(languageServer, initializeParams, cancellationToken) =>
|
||||
{
|
||||
// Wire up the HostLogger to the LanguageServer's logger once we are initialized, so that any messages still logged to the HostLogger get sent across the LSP channel. There is no more logging to disk at this point.
|
||||
hostLoggerSubscription = HostLogger.Subscribe(new HostLoggerAdapter(
|
||||
languageServer.Services.GetService<ILogger<HostLoggerAdapter>>()
|
||||
));
|
||||
|
||||
// Set the workspace path from the parameters.
|
||||
WorkspaceService workspaceService = languageServer.Services.GetService<WorkspaceService>();
|
||||
if (initializeParams.WorkspaceFolders is not null)
|
||||
{
|
||||
workspaceService.WorkspaceFolders.AddRange(initializeParams.WorkspaceFolders);
|
||||
}
|
||||
|
||||
// Parse initialization options.
|
||||
JObject initializationOptions = initializeParams.InitializationOptions as JObject;
|
||||
HostStartOptions hostStartOptions = new()
|
||||
{
|
||||
// TODO: We need to synchronize our "default" settings as specified
|
||||
// in the VS Code extension's package.json with the actual default
|
||||
// values in this project. For now, this is going to be the most
|
||||
// annoying setting, so we're defaulting this to true.
|
||||
//
|
||||
// NOTE: The keys start with a lowercase because OmniSharp's client
|
||||
// (used for testing) forces it to be that way.
|
||||
LoadProfiles = initializationOptions?.GetValue("enableProfileLoading")?.Value<bool>()
|
||||
?? true,
|
||||
// First check the setting, then use the first workspace folder,
|
||||
// finally fall back to CWD.
|
||||
InitialWorkingDirectory = initializationOptions?.GetValue("initialWorkingDirectory")?.Value<string>()
|
||||
?? workspaceService.WorkspaceFolders.FirstOrDefault()?.Uri.GetFileSystemPath()
|
||||
?? Directory.GetCurrentDirectory(),
|
||||
// If a shell integration script path is provided, that implies the feature is enabled.
|
||||
ShellIntegrationScript = initializationOptions?.GetValue("shellIntegrationScript")?.Value<string>()
|
||||
?? "",
|
||||
};
|
||||
|
||||
workspaceService.InitialWorkingDirectory = hostStartOptions.InitialWorkingDirectory;
|
||||
|
||||
_psesHost = languageServer.Services.GetService<PsesInternalHost>();
|
||||
return _psesHost.TryStartAsync(hostStartOptions, cancellationToken);
|
||||
}
|
||||
)
|
||||
.OnShutdown(_ => hostLoggerSubscription.Dispose());
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
_serverStart.SetResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a task that completes when the server is shut down.
|
||||
/// </summary>
|
||||
/// <returns>A task that completes when the server is shut down.</returns>
|
||||
public async Task WaitForShutdown()
|
||||
{
|
||||
await _serverStart.Task.ConfigureAwait(false);
|
||||
await LanguageServer.WaitForExit.ConfigureAwait(false);
|
||||
|
||||
// Doing this means we're able to route through any exceptions experienced on the pipeline thread
|
||||
_psesHost.TriggerShutdown();
|
||||
await _psesHost.Shutdown.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerShell.EditorServices.Hosting;
|
||||
using Microsoft.PowerShell.EditorServices.Services;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Extension;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
|
||||
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Server
|
||||
{
|
||||
internal static class PsesServiceCollectionExtensions
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "Using lazy initialization.")]
|
||||
public static IServiceCollection AddPsesLanguageServices(
|
||||
this IServiceCollection collection,
|
||||
HostStartupInfo hostStartupInfo)
|
||||
{
|
||||
return collection
|
||||
.AddSingleton(hostStartupInfo)
|
||||
.AddSingleton<WorkspaceService>()
|
||||
.AddSingleton<SymbolsService>()
|
||||
.AddSingleton<PsesInternalHost>()
|
||||
.AddSingleton<IRunspaceContext>(
|
||||
(provider) => provider.GetService<PsesInternalHost>())
|
||||
.AddSingleton<IInternalPowerShellExecutionService>(
|
||||
(provider) => provider.GetService<PsesInternalHost>())
|
||||
.AddSingleton<ConfigurationService>()
|
||||
.AddSingleton<IPowerShellDebugContext>(
|
||||
(provider) => provider.GetService<PsesInternalHost>().DebugContext)
|
||||
.AddSingleton<EditorOperationsService>()
|
||||
.AddSingleton<RemoteFileManagerService>()
|
||||
.AddSingleton((provider) =>
|
||||
{
|
||||
ExtensionService extensionService = new(
|
||||
provider.GetService<ILanguageServerFacade>(),
|
||||
provider,
|
||||
provider.GetService<EditorOperationsService>(),
|
||||
provider.GetService<IInternalPowerShellExecutionService>());
|
||||
|
||||
// This is where we create the $psEditor variable so that when the console
|
||||
// is ready, it will be available. NOTE: We cannot await this because it
|
||||
// uses a lazy initialization to avoid a race with the dependency injection
|
||||
// framework, see the EditorObject class for that!
|
||||
extensionService.InitializeAsync();
|
||||
return extensionService;
|
||||
})
|
||||
.AddSingleton<AnalysisService>();
|
||||
}
|
||||
|
||||
public static IServiceCollection AddPsesDebugServices(
|
||||
this IServiceCollection collection,
|
||||
IServiceProvider languageServiceProvider,
|
||||
PsesDebugServer psesDebugServer)
|
||||
{
|
||||
PsesInternalHost internalHost = languageServiceProvider.GetService<PsesInternalHost>();
|
||||
|
||||
return collection
|
||||
.AddSingleton(internalHost)
|
||||
.AddSingleton<IRunspaceContext>(internalHost)
|
||||
.AddSingleton<IPowerShellDebugContext>(internalHost.DebugContext)
|
||||
.AddSingleton(languageServiceProvider.GetService<IInternalPowerShellExecutionService>())
|
||||
.AddSingleton(languageServiceProvider.GetService<WorkspaceService>())
|
||||
.AddSingleton(languageServiceProvider.GetService<RemoteFileManagerService>())
|
||||
.AddSingleton(psesDebugServer)
|
||||
.AddSingleton<DebugService>()
|
||||
.AddSingleton<BreakpointService>()
|
||||
.AddSingleton<DebugStateService>()
|
||||
.AddSingleton<DebugEventHandlerService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,519 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.PowerShell.EditorServices.Hosting;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Analysis;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Configuration;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a high-level service for performing semantic analysis
|
||||
/// of PowerShell scripts.
|
||||
/// </summary>
|
||||
internal class AnalysisService : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Reliably generate an ID for a diagnostic record to track it.
|
||||
/// </summary>
|
||||
/// <param name="diagnostic">The diagnostic to generate an ID for.</param>
|
||||
/// <returns>A string unique to this diagnostic given where and what kind it is.</returns>
|
||||
internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic)
|
||||
{
|
||||
Position start = diagnostic.Range.Start;
|
||||
Position end = diagnostic.Range.End;
|
||||
|
||||
StringBuilder sb = new StringBuilder(256)
|
||||
.Append(diagnostic.Source ?? "?")
|
||||
.Append('_')
|
||||
.Append(diagnostic.Code?.IsString ?? true ? diagnostic.Code?.String : diagnostic.Code?.Long.ToString())
|
||||
.Append('_')
|
||||
.Append(diagnostic.Severity?.ToString() ?? "?")
|
||||
.Append('_')
|
||||
.Append(start.Line)
|
||||
.Append(':')
|
||||
.Append(start.Character)
|
||||
.Append('-')
|
||||
.Append(end.Line)
|
||||
.Append(':')
|
||||
.Append(end.Character);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the list of Script Analyzer rules to include by default if
|
||||
/// no settings file is specified.
|
||||
/// </summary>
|
||||
internal static readonly string[] s_defaultRules = {
|
||||
"PSAvoidAssignmentToAutomaticVariable",
|
||||
"PSUseToExportFieldsInManifest",
|
||||
"PSMisleadingBacktick",
|
||||
"PSAvoidUsingCmdletAliases",
|
||||
"PSUseApprovedVerbs",
|
||||
"PSAvoidUsingPlainTextForPassword",
|
||||
"PSReservedCmdletChar",
|
||||
"PSReservedParams",
|
||||
"PSShouldProcess",
|
||||
"PSMissingModuleManifestField",
|
||||
"PSAvoidDefaultValueSwitchParameter",
|
||||
"PSUseDeclaredVarsMoreThanAssignments",
|
||||
"PSPossibleIncorrectComparisonWithNull",
|
||||
"PSAvoidDefaultValueForMandatoryParameter",
|
||||
"PSPossibleIncorrectUsageOfRedirectionOperator"
|
||||
};
|
||||
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly ILanguageServerFacade _languageServer;
|
||||
|
||||
private readonly ConfigurationService _configurationService;
|
||||
|
||||
private readonly WorkspaceService _workspaceService;
|
||||
|
||||
private readonly int _analysisDelayMillis = 750;
|
||||
|
||||
private readonly ConcurrentDictionary<ScriptFile, CorrectionTableEntry> _mostRecentCorrectionsByFile = new();
|
||||
|
||||
private Lazy<PssaCmdletAnalysisEngine> _analysisEngineLazy;
|
||||
|
||||
private CancellationTokenSource _diagnosticsCancellationTokenSource;
|
||||
|
||||
private readonly string _pssaModulePath;
|
||||
|
||||
private string _pssaSettingsFilePath;
|
||||
|
||||
public AnalysisService(
|
||||
ILoggerFactory loggerFactory,
|
||||
ILanguageServerFacade languageServer,
|
||||
ConfigurationService configurationService,
|
||||
WorkspaceService workspaceService,
|
||||
HostStartupInfo hostInfo)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = loggerFactory.CreateLogger<AnalysisService>();
|
||||
_languageServer = languageServer;
|
||||
_configurationService = configurationService;
|
||||
_workspaceService = workspaceService;
|
||||
_analysisEngineLazy = new Lazy<PssaCmdletAnalysisEngine>(InstantiateAnalysisEngine);
|
||||
_pssaModulePath = Path.Combine(hostInfo.BundledModulePath, "PSScriptAnalyzer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The analysis engine to use for running script analysis.
|
||||
/// </summary>
|
||||
internal PssaCmdletAnalysisEngine AnalysisEngine => _analysisEngineLazy?.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up a script analysis run, eventually returning the result.
|
||||
/// </summary>
|
||||
/// <param name="filesToAnalyze">The files to run script analysis on.</param>
|
||||
/// <returns>A task that finishes when script diagnostics have been published.</returns>
|
||||
public void StartScriptDiagnostics(ScriptFile[] filesToAnalyze)
|
||||
{
|
||||
if (!_configurationService.CurrentSettings.ScriptAnalysis.Enable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureEngineSettingsCurrent();
|
||||
|
||||
// If there's an existing task, we want to cancel it here;
|
||||
CancellationTokenSource cancellationSource = new();
|
||||
CancellationTokenSource oldTaskCancellation = Interlocked.Exchange(ref _diagnosticsCancellationTokenSource, cancellationSource);
|
||||
if (oldTaskCancellation is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
oldTaskCancellation.Cancel();
|
||||
oldTaskCancellation.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Exception occurred while cancelling analysis task");
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToAnalyze.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Task analysisTask = Task.Run(() => DelayThenInvokeDiagnosticsAsync(filesToAnalyze, _diagnosticsCancellationTokenSource.Token), _diagnosticsCancellationTokenSource.Token);
|
||||
|
||||
// Ensure that any next corrections request will wait for this diagnostics publication
|
||||
foreach (ScriptFile file in filesToAnalyze)
|
||||
{
|
||||
CorrectionTableEntry fileCorrectionsEntry = _mostRecentCorrectionsByFile.GetOrAdd(
|
||||
file,
|
||||
CorrectionTableEntry.CreateForFile);
|
||||
|
||||
fileCorrectionsEntry.DiagnosticPublish = analysisTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a PowerShell script with the given settings.
|
||||
/// </summary>
|
||||
/// <param name="scriptFileContents">The script to format.</param>
|
||||
/// <param name="formatSettings">The settings to use with the formatter.</param>
|
||||
/// <param name="formatRange">Optionally, the range that should be formatted.</param>
|
||||
/// <returns>The text of the formatted PowerShell script.</returns>
|
||||
public Task<string> FormatAsync(string scriptFileContents, Hashtable formatSettings, int[] formatRange = null)
|
||||
{
|
||||
EnsureEngineSettingsCurrent();
|
||||
return AnalysisEngine.FormatAsync(scriptFileContents, formatSettings, formatRange);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get comment help text for a PowerShell function definition.
|
||||
/// </summary>
|
||||
/// <param name="functionText">The text of the function to get comment help for.</param>
|
||||
/// <param name="helpLocation">A string referring to which location comment help should be placed around the function.</param>
|
||||
/// <param name="forBlockComment">If true, block comment help will be supplied.</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> GetCommentHelpText(string functionText, string helpLocation, bool forBlockComment)
|
||||
{
|
||||
if (AnalysisEngine is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Hashtable commentHelpSettings = GetCommentHelpRuleSettings(helpLocation, forBlockComment);
|
||||
|
||||
ScriptFileMarker[] analysisResults = await AnalysisEngine.AnalyzeScriptAsync(functionText, commentHelpSettings).ConfigureAwait(false);
|
||||
|
||||
if (analysisResults.Length == 0 || !analysisResults[0].Corrections.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return analysisResults[0].Corrections.First().Edit.Text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the most recent corrections computed for a given script file.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI string of the file to get code actions for.</param>
|
||||
/// <returns>A thread-safe readonly dictionary of the code actions of the particular file.</returns>
|
||||
public async Task<IReadOnlyDictionary<string, IEnumerable<MarkerCorrection>>> GetMostRecentCodeActionsForFileAsync(DocumentUri uri)
|
||||
{
|
||||
if (!_workspaceService.TryGetFile(uri, out ScriptFile file)
|
||||
|| !_mostRecentCorrectionsByFile.TryGetValue(file, out CorrectionTableEntry corrections))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Wait for diagnostics to be published for this file
|
||||
await corrections.DiagnosticPublish.ConfigureAwait(false);
|
||||
|
||||
return corrections.Corrections;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all diagnostic markers for a given file.
|
||||
/// </summary>
|
||||
/// <param name="file">The file to clear markers in.</param>
|
||||
/// <returns>A task that ends when all markers in the file have been cleared.</returns>
|
||||
public void ClearMarkers(ScriptFile file) => PublishScriptDiagnostics(file, new List<ScriptFileMarker>());
|
||||
|
||||
/// <summary>
|
||||
/// Event subscription method to be run when PSES configuration has been updated.
|
||||
/// </summary>
|
||||
/// <param name="_">The sender of the configuration update event.</param>
|
||||
/// <param name="settings">The new language server settings.</param>
|
||||
public void OnConfigurationUpdated(object _, LanguageServerSettings settings)
|
||||
{
|
||||
if (settings.ScriptAnalysis.Enable)
|
||||
{
|
||||
InitializeAnalysisEngineToCurrentSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureEngineSettingsCurrent()
|
||||
{
|
||||
if (_analysisEngineLazy is null
|
||||
|| (_pssaSettingsFilePath is not null
|
||||
&& !File.Exists(_pssaSettingsFilePath)))
|
||||
{
|
||||
InitializeAnalysisEngineToCurrentSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeAnalysisEngineToCurrentSettings()
|
||||
{
|
||||
// We may be triggered after the lazy factory is set,
|
||||
// but before it's been able to instantiate
|
||||
if (_analysisEngineLazy is null)
|
||||
{
|
||||
_analysisEngineLazy = new Lazy<PssaCmdletAnalysisEngine>(InstantiateAnalysisEngine);
|
||||
return;
|
||||
}
|
||||
else if (!_analysisEngineLazy.IsValueCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the current script analysis engine so we can recreate it after we've overridden it
|
||||
PssaCmdletAnalysisEngine currentAnalysisEngine = AnalysisEngine;
|
||||
|
||||
// Clear the open file markers and set the new engine factory
|
||||
ClearOpenFileMarkers();
|
||||
_analysisEngineLazy = new Lazy<PssaCmdletAnalysisEngine>(() => RecreateAnalysisEngine(currentAnalysisEngine));
|
||||
}
|
||||
|
||||
internal PssaCmdletAnalysisEngine InstantiateAnalysisEngine()
|
||||
{
|
||||
PssaCmdletAnalysisEngine.Builder pssaCmdletEngineBuilder = new(_loggerFactory);
|
||||
|
||||
// If there's a settings file use that
|
||||
if (TryFindSettingsFile(out string settingsFilePath))
|
||||
{
|
||||
_logger.LogInformation($"Configuring PSScriptAnalyzer with rules at '{settingsFilePath}'");
|
||||
_pssaSettingsFilePath = settingsFilePath;
|
||||
pssaCmdletEngineBuilder.WithSettingsFile(settingsFilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("PSScriptAnalyzer settings file not found. Falling back to default rules");
|
||||
pssaCmdletEngineBuilder.WithIncludedRules(s_defaultRules);
|
||||
}
|
||||
|
||||
return pssaCmdletEngineBuilder.Build(_pssaModulePath);
|
||||
}
|
||||
|
||||
private PssaCmdletAnalysisEngine RecreateAnalysisEngine(PssaCmdletAnalysisEngine oldAnalysisEngine)
|
||||
{
|
||||
if (TryFindSettingsFile(out string settingsFilePath))
|
||||
{
|
||||
_logger.LogInformation($"Recreating analysis engine with rules at '{settingsFilePath}'");
|
||||
_pssaSettingsFilePath = settingsFilePath;
|
||||
return oldAnalysisEngine.RecreateWithNewSettings(settingsFilePath);
|
||||
}
|
||||
|
||||
_logger.LogInformation("PSScriptAnalyzer settings file not found. Falling back to default rules");
|
||||
return oldAnalysisEngine.RecreateWithRules(s_defaultRules);
|
||||
}
|
||||
|
||||
private bool TryFindSettingsFile(out string settingsFilePath)
|
||||
{
|
||||
string configuredPath = _configurationService?.CurrentSettings.ScriptAnalysis.SettingsPath;
|
||||
|
||||
if (string.IsNullOrEmpty(configuredPath))
|
||||
{
|
||||
settingsFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
settingsFilePath = _workspaceService?.FindFileInWorkspace(configuredPath);
|
||||
|
||||
if (settingsFilePath is null
|
||||
|| !File.Exists(settingsFilePath))
|
||||
{
|
||||
_logger.LogInformation($"Unable to find PSSA settings file at '{configuredPath}'. Loading default rules.");
|
||||
settingsFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Found PSSA settings file at '{settingsFilePath}'");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ClearOpenFileMarkers()
|
||||
{
|
||||
foreach (ScriptFile file in _workspaceService.GetOpenedFiles())
|
||||
{
|
||||
ClearMarkers(file);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task DelayThenInvokeDiagnosticsAsync(ScriptFile[] filesToAnalyze, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(_analysisDelayMillis, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If we've made it past the delay period then we don't care
|
||||
// about the cancellation token anymore. This could happen
|
||||
// when the user stops typing for long enough that the delay
|
||||
// period ends but then starts typing while analysis is going
|
||||
// on. It makes sense to send back the results from the first
|
||||
// delay period while the second one is ticking away.
|
||||
|
||||
foreach (ScriptFile scriptFile in filesToAnalyze)
|
||||
{
|
||||
ScriptFileMarker[] semanticMarkers = await AnalysisEngine.AnalyzeScriptAsync(scriptFile.Contents).ConfigureAwait(false);
|
||||
|
||||
// Clear existing PSScriptAnalyzer markers (but keep parser errors where the source is "PowerShell")
|
||||
// so that they are not duplicated when re-opening files.
|
||||
scriptFile.DiagnosticMarkers.RemoveAll(m => m.Source == "PSScriptAnalyzer");
|
||||
scriptFile.DiagnosticMarkers.AddRange(semanticMarkers);
|
||||
|
||||
PublishScriptDiagnostics(scriptFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishScriptDiagnostics(ScriptFile scriptFile) => PublishScriptDiagnostics(scriptFile, scriptFile.DiagnosticMarkers);
|
||||
|
||||
private void PublishScriptDiagnostics(ScriptFile scriptFile, List<ScriptFileMarker> markers)
|
||||
{
|
||||
// NOTE: Sometimes we have null markers for reasons we don't yet know, but we need to
|
||||
// remove them.
|
||||
_ = markers.RemoveAll(m => m is null);
|
||||
Diagnostic[] diagnostics = new Diagnostic[markers.Count];
|
||||
|
||||
CorrectionTableEntry fileCorrections = _mostRecentCorrectionsByFile.GetOrAdd(
|
||||
scriptFile,
|
||||
CorrectionTableEntry.CreateForFile);
|
||||
|
||||
fileCorrections.Corrections.Clear();
|
||||
|
||||
for (int i = 0; i < markers.Count; i++)
|
||||
{
|
||||
ScriptFileMarker marker = markers[i];
|
||||
|
||||
Diagnostic diagnostic = GetDiagnosticFromMarker(marker);
|
||||
|
||||
if (marker.Corrections is not null)
|
||||
{
|
||||
string diagnosticId = GetUniqueIdFromDiagnostic(diagnostic);
|
||||
fileCorrections.Corrections[diagnosticId] = marker.Corrections;
|
||||
}
|
||||
|
||||
diagnostics[i] = diagnostic;
|
||||
}
|
||||
|
||||
_languageServer?.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams
|
||||
{
|
||||
Uri = scriptFile.DocumentUri,
|
||||
Diagnostics = new Container<Diagnostic>(diagnostics)
|
||||
});
|
||||
}
|
||||
|
||||
private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker)
|
||||
{
|
||||
return new Diagnostic
|
||||
{
|
||||
Severity = MapDiagnosticSeverity(scriptFileMarker.Level),
|
||||
Message = scriptFileMarker.Message,
|
||||
Code = scriptFileMarker.RuleName,
|
||||
Source = scriptFileMarker.Source,
|
||||
Range = new Range
|
||||
{
|
||||
Start = new Position
|
||||
{
|
||||
Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1,
|
||||
Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1
|
||||
},
|
||||
End = new Position
|
||||
{
|
||||
Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1,
|
||||
Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel)
|
||||
{
|
||||
return markerLevel switch
|
||||
{
|
||||
ScriptFileMarkerLevel.Error => DiagnosticSeverity.Error,
|
||||
ScriptFileMarkerLevel.Warning => DiagnosticSeverity.Warning,
|
||||
ScriptFileMarkerLevel.Information => DiagnosticSeverity.Information,
|
||||
_ => DiagnosticSeverity.Error,
|
||||
};
|
||||
}
|
||||
|
||||
private static Hashtable GetCommentHelpRuleSettings(string helpLocation, bool forBlockComment)
|
||||
{
|
||||
return new Hashtable {
|
||||
{ "Rules", new Hashtable {
|
||||
{ "PSProvideCommentHelp", new Hashtable {
|
||||
{ "Enable", true },
|
||||
{ "ExportedOnly", false },
|
||||
{ "BlockComment", forBlockComment },
|
||||
{ "VSCodeSnippetCorrection", true },
|
||||
{ "Placement", helpLocation }}
|
||||
}}},
|
||||
{ "IncludeRules", "PSProvideCommentHelp" }};
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
private bool disposedValue; // To detect redundant calls
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (_analysisEngineLazy?.IsValueCreated == true)
|
||||
{
|
||||
_analysisEngineLazy.Value.Dispose();
|
||||
}
|
||||
|
||||
_diagnosticsCancellationTokenSource?.Dispose();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
// This code added to correctly implement the disposable pattern.
|
||||
public void Dispose() =>
|
||||
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
|
||||
Dispose(true);
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Tracks corrections suggested by PSSA for a given file,
|
||||
/// so that after a diagnostics request has fired,
|
||||
/// a code action request can look up that file,
|
||||
/// wait for analysis to finish if needed,
|
||||
/// and then fetch the corrections set in the table entry by PSSA.
|
||||
/// </summary>
|
||||
private class CorrectionTableEntry
|
||||
{
|
||||
public static CorrectionTableEntry CreateForFile(ScriptFile _) => new();
|
||||
|
||||
public CorrectionTableEntry()
|
||||
{
|
||||
Corrections = new ConcurrentDictionary<string, IEnumerable<MarkerCorrection>>();
|
||||
DiagnosticPublish = Task.CompletedTask;
|
||||
}
|
||||
|
||||
public ConcurrentDictionary<string, IEnumerable<MarkerCorrection>> Corrections { get; }
|
||||
|
||||
public Task DiagnosticPublish { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using Microsoft.PowerShell.EditorServices.Utility;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.Services.Analysis
|
||||
{
|
||||
using System.Management.Automation;
|
||||
using System.Management.Automation.Runspaces;
|
||||
|
||||
/// <summary>
|
||||
/// PowerShell script analysis engine that uses PSScriptAnalyzer
|
||||
/// cmdlets run through a PowerShell API to drive analysis.
|
||||
/// </summary>
|
||||
internal class PssaCmdletAnalysisEngine : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Builder for the PssaCmdletAnalysisEngine allowing settings configuration.
|
||||
/// </summary>
|
||||
public class Builder
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
private object _settingsParameter;
|
||||
|
||||
private string[] _rules;
|
||||
|
||||
/// <summary>
|
||||
/// Create a builder for PssaCmdletAnalysisEngine construction.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The logger to use.</param>
|
||||
public Builder(ILoggerFactory loggerFactory) => _loggerFactory = loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Uses a settings file for PSSA rule configuration.
|
||||
/// </summary>
|
||||
/// <param name="settingsPath">The absolute path to the settings file.</param>
|
||||
/// <returns>The builder for chaining.</returns>
|
||||
public Builder WithSettingsFile(string settingsPath)
|
||||
{
|
||||
_settingsParameter = settingsPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses a set of unconfigured rules for PSSA configuration.
|
||||
/// </summary>
|
||||
/// <param name="rules">The rules for PSSA to run.</param>
|
||||
/// <returns>The builder for chaining.</returns>
|
||||
public Builder WithIncludedRules(string[] rules)
|
||||
{
|
||||
_rules = rules;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to build a PssaCmdletAnalysisEngine with the given configuration.
|
||||
/// If PSScriptAnalyzer cannot be found, this will return null.
|
||||
/// </summary>
|
||||
/// <returns>A newly configured PssaCmdletAnalysisEngine, or null if PSScriptAnalyzer cannot be found.</returns>
|
||||
public PssaCmdletAnalysisEngine Build(string pssaModulePath)
|
||||
{
|
||||
// RunspacePool takes care of queuing commands for us so we do not
|
||||
// need to worry about executing concurrent commands
|
||||
ILogger logger = _loggerFactory.CreateLogger<PssaCmdletAnalysisEngine>();
|
||||
|
||||
logger.LogDebug("Creating PSScriptAnalyzer runspace with module at: '{Path}'", pssaModulePath);
|
||||
RunspacePool pssaRunspacePool = CreatePssaRunspacePool(pssaModulePath);
|
||||
PssaCmdletAnalysisEngine cmdletAnalysisEngine = new(logger, pssaRunspacePool, _rules, _settingsParameter);
|
||||
cmdletAnalysisEngine.LogAvailablePssaFeatures();
|
||||
return cmdletAnalysisEngine;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The indentation to add when the logger lists errors.
|
||||
/// </summary>
|
||||
private static readonly string s_indentJoin = Environment.NewLine + " ";
|
||||
|
||||
private static readonly IReadOnlyCollection<PSObject> s_emptyDiagnosticResult = new Collection<PSObject>();
|
||||
|
||||
private static readonly ScriptFileMarkerLevel[] s_scriptMarkerLevels = new[]
|
||||
{
|
||||
ScriptFileMarkerLevel.Error,
|
||||
ScriptFileMarkerLevel.Warning,
|
||||
ScriptFileMarkerLevel.Information
|
||||
};
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly RunspacePool _analysisRunspacePool;
|
||||
|
||||
internal readonly object _settingsParameter;
|
||||
|
||||
internal readonly string[] _rulesToInclude;
|
||||
|
||||
private PssaCmdletAnalysisEngine(
|
||||
ILogger logger,
|
||||
RunspacePool analysisRunspacePool,
|
||||
string[] rulesToInclude = default,
|
||||
object analysisSettingsParameter = default)
|
||||
{
|
||||
_logger = logger;
|
||||
_analysisRunspacePool = analysisRunspacePool;
|
||||
_rulesToInclude = rulesToInclude;
|
||||
_settingsParameter = analysisSettingsParameter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a script given its contents.
|
||||
/// TODO: This needs to be cancellable.
|
||||
/// </summary>
|
||||
/// <param name="scriptDefinition">The full text of a script.</param>
|
||||
/// <param name="formatSettings">The formatter settings to use.</param>
|
||||
/// <param name="rangeList">A possible range over which to run the formatter.</param>
|
||||
/// <returns>Formatted script as string</returns>
|
||||
public async Task<string> FormatAsync(string scriptDefinition, Hashtable formatSettings, int[] rangeList)
|
||||
{
|
||||
// We cannot use Range type therefore this workaround of using -1 default value.
|
||||
// Invoke-Formatter throws a ParameterBinderValidationException if the ScriptDefinition is an empty string.
|
||||
if (string.IsNullOrEmpty(scriptDefinition))
|
||||
{
|
||||
_logger.LogDebug("Script Definition was: " + scriptDefinition is null ? "null" : "empty string");
|
||||
return scriptDefinition;
|
||||
}
|
||||
|
||||
PSCommand psCommand = new PSCommand()
|
||||
.AddCommand("Invoke-Formatter")
|
||||
.AddParameter("ScriptDefinition", scriptDefinition)
|
||||
.AddParameter("Settings", formatSettings);
|
||||
|
||||
if (rangeList is not null)
|
||||
{
|
||||
psCommand.AddParameter("Range", rangeList);
|
||||
}
|
||||
|
||||
PowerShellResult result = await InvokePowerShellAsync(psCommand).ConfigureAwait(false);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
_logger.LogError("Formatter returned null result");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (result.HasErrors)
|
||||
{
|
||||
StringBuilder errorBuilder = new StringBuilder().Append(s_indentJoin);
|
||||
foreach (ErrorRecord err in result.Errors)
|
||||
{
|
||||
errorBuilder.Append(err).Append(s_indentJoin);
|
||||
}
|
||||
_logger.LogWarning($"Errors found while formatting file: {errorBuilder}");
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (PSObject resultObj in result.Output)
|
||||
{
|
||||
if (resultObj?.BaseObject is string formatResult)
|
||||
{
|
||||
return formatResult;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogError("Couldn't get result from output. Returning null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyze a given script using PSScriptAnalyzer.
|
||||
/// </summary>
|
||||
/// <param name="scriptContent">The contents of the script to analyze.</param>
|
||||
/// <returns>An array of markers indicating script analysis diagnostics.</returns>
|
||||
public Task<ScriptFileMarker[]> AnalyzeScriptAsync(string scriptContent) => AnalyzeScriptAsync(scriptContent, settings: null);
|
||||
|
||||
/// <summary>
|
||||
/// Analyze a given script using PSScriptAnalyzer.
|
||||
/// </summary>
|
||||
/// <param name="scriptContent">The contents of the script to analyze.</param>
|
||||
/// <param name="settings">The settings file to use in this instance of analysis.</param>
|
||||
/// <returns>An array of markers indicating script analysis diagnostics.</returns>
|
||||
public Task<ScriptFileMarker[]> AnalyzeScriptAsync(string scriptContent, Hashtable settings)
|
||||
{
|
||||
// When a new, empty file is created there are by definition no issues.
|
||||
// Furthermore, if you call Invoke-ScriptAnalyzer with an empty ScriptDefinition
|
||||
// it will generate a ParameterBindingValidationException.
|
||||
if (string.IsNullOrEmpty(scriptContent))
|
||||
{
|
||||
return Task.FromResult(Array.Empty<ScriptFileMarker>());
|
||||
}
|
||||
|
||||
PSCommand command = new PSCommand()
|
||||
.AddCommand("Invoke-ScriptAnalyzer")
|
||||
.AddParameter("ScriptDefinition", scriptContent)
|
||||
.AddParameter("Severity", s_scriptMarkerLevels);
|
||||
|
||||
object settingsValue = settings ?? _settingsParameter;
|
||||
if (settingsValue is not null)
|
||||
{
|
||||
command.AddParameter("Settings", settingsValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
command.AddParameter("IncludeRule", _rulesToInclude);
|
||||
}
|
||||
|
||||
return GetSemanticMarkersFromCommandAsync(command);
|
||||
}
|
||||
|
||||
public PssaCmdletAnalysisEngine RecreateWithNewSettings(string settingsPath) => new(
|
||||
_logger,
|
||||
_analysisRunspacePool,
|
||||
rulesToInclude: null,
|
||||
analysisSettingsParameter: settingsPath);
|
||||
|
||||
public PssaCmdletAnalysisEngine RecreateWithRules(string[] rules) => new(
|
||||
_logger,
|
||||
_analysisRunspacePool,
|
||||
rulesToInclude: rules,
|
||||
analysisSettingsParameter: null);
|
||||
|
||||
#region IDisposable Support
|
||||
private bool disposedValue; // To detect redundant calls
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_analysisRunspacePool.Dispose();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
// This code added to correctly implement the disposable pattern.
|
||||
public void Dispose() =>
|
||||
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
|
||||
Dispose(true);
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<ScriptFileMarker[]> GetSemanticMarkersFromCommandAsync(PSCommand command)
|
||||
{
|
||||
PowerShellResult result = await InvokePowerShellAsync(command).ConfigureAwait(false);
|
||||
|
||||
IReadOnlyCollection<PSObject> diagnosticResults = result?.Output ?? s_emptyDiagnosticResult;
|
||||
_logger.LogDebug(string.Format("Found {0} violations", diagnosticResults.Count));
|
||||
|
||||
ScriptFileMarker[] scriptMarkers = new ScriptFileMarker[diagnosticResults.Count];
|
||||
int i = 0;
|
||||
foreach (PSObject diagnostic in diagnosticResults)
|
||||
{
|
||||
scriptMarkers[i] = ScriptFileMarker.FromDiagnosticRecord(diagnostic);
|
||||
i++;
|
||||
}
|
||||
|
||||
return scriptMarkers;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate this logic and cleanup using lessons learned from pipeline rewrite.
|
||||
private Task<PowerShellResult> InvokePowerShellAsync(PSCommand command) => Task.Run(() => InvokePowerShell(command));
|
||||
|
||||
private PowerShellResult InvokePowerShell(PSCommand command)
|
||||
{
|
||||
using PowerShell pwsh = PowerShell.Create(RunspaceMode.NewRunspace);
|
||||
pwsh.RunspacePool = _analysisRunspacePool;
|
||||
pwsh.Commands = command;
|
||||
PowerShellResult result = null;
|
||||
try
|
||||
{
|
||||
Collection<PSObject> output = pwsh.Invoke();
|
||||
PSDataCollection<ErrorRecord> errors = pwsh.Streams.Error;
|
||||
result = new PowerShellResult(output, errors, pwsh.HadErrors);
|
||||
}
|
||||
catch (CommandNotFoundException ex)
|
||||
{
|
||||
// This exception is possible if the module path loaded
|
||||
// is wrong even though PSScriptAnalyzer is available as a module
|
||||
_logger.LogError(ex.Message);
|
||||
}
|
||||
catch (CmdletInvocationException ex)
|
||||
{
|
||||
// We do not want to crash EditorServices for exceptions caused by cmdlet invocation.
|
||||
// The main reasons that cause the exception are:
|
||||
// * PSCmdlet.WriteOutput being called from another thread than Begin/Process
|
||||
// * CompositionContainer.ComposeParts complaining that "...Only one batch can be composed at a time"
|
||||
// * PSScriptAnalyzer not being able to find its PSScriptAnalyzer.psd1 because we are hosted by an Assembly other than pwsh.exe
|
||||
string message = ex.Message;
|
||||
if (!string.IsNullOrEmpty(ex.ErrorRecord.FullyQualifiedErrorId))
|
||||
{
|
||||
// Microsoft.PowerShell.EditorServices.Services.Analysis.PssaCmdletAnalysisEngine: Exception of type 'System.Exception' was thrown. |
|
||||
message += $" | {ex.ErrorRecord.FullyQualifiedErrorId}";
|
||||
}
|
||||
_logger.LogError(message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log the features available from the PSScriptAnalyzer module that has been imported
|
||||
/// for use with the AnalysisService.
|
||||
/// </summary>
|
||||
private void LogAvailablePssaFeatures()
|
||||
{
|
||||
// Save ourselves some work here
|
||||
if (!_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine("PSScriptAnalyzer successfully imported:").AppendLine(" Available Rules:");
|
||||
|
||||
// Log available rules
|
||||
foreach (string ruleName in GetPSScriptAnalyzerRules())
|
||||
{
|
||||
sb.Append(" ").AppendLine(ruleName);
|
||||
}
|
||||
|
||||
_logger.LogDebug(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of builtin-in PSScriptAnalyzer rules
|
||||
/// </summary>
|
||||
private IEnumerable<string> GetPSScriptAnalyzerRules()
|
||||
{
|
||||
PowerShellResult getRuleResult = InvokePowerShell(new PSCommand().AddCommand("Get-ScriptAnalyzerRule"));
|
||||
if (getRuleResult is null)
|
||||
{
|
||||
_logger.LogWarning("Get-ScriptAnalyzerRule returned null result");
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
List<string> ruleNames = new(getRuleResult.Output.Count);
|
||||
foreach (PSObject rule in getRuleResult.Output)
|
||||
{
|
||||
ruleNames.Add((string)rule.Members["RuleName"].Value);
|
||||
}
|
||||
|
||||
return ruleNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new runspace pool around a PSScriptAnalyzer module for asynchronous script analysis tasks.
|
||||
/// This looks for the latest version of PSScriptAnalyzer on the path and loads that.
|
||||
/// </summary>
|
||||
/// <returns>A runspace pool with PSScriptAnalyzer loaded for running script analysis tasks.</returns>
|
||||
private static RunspacePool CreatePssaRunspacePool(string pssaModulePath)
|
||||
{
|
||||
using PowerShell pwsh = PowerShell.Create(RunspaceMode.NewRunspace);
|
||||
|
||||
// Now that we know where the PSScriptAnalyzer we want to use is, create a base
|
||||
// session state with PSScriptAnalyzer loaded
|
||||
//
|
||||
// We intentionally use `CreateDefault2()` as it loads `Microsoft.PowerShell.Core`
|
||||
// only, which is a more minimal and therefore safer state.
|
||||
InitialSessionState sessionState = InitialSessionState.CreateDefault2();
|
||||
|
||||
// We set the runspace's execution policy `Bypass` so we can always import our bundled
|
||||
// PSScriptAnalyzer module.
|
||||
if (VersionUtils.IsWindows)
|
||||
{
|
||||
sessionState.ExecutionPolicy = ExecutionPolicy.Bypass;
|
||||
}
|
||||
|
||||
sessionState.ImportPSModulesFromPath(pssaModulePath);
|
||||
|
||||
RunspacePool runspacePool = RunspaceFactory.CreateRunspacePool(sessionState);
|
||||
|
||||
runspacePool.SetMaxRunspaces(1);
|
||||
runspacePool.ThreadOptions = PSThreadOptions.ReuseThread;
|
||||
runspacePool.Open();
|
||||
|
||||
return runspacePool;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps the result of an execution of PowerShell to send back through
|
||||
/// asynchronous calls.
|
||||
/// </summary>
|
||||
private class PowerShellResult
|
||||
{
|
||||
public PowerShellResult(
|
||||
Collection<PSObject> output,
|
||||
PSDataCollection<ErrorRecord> errors,
|
||||
bool hasErrors)
|
||||
{
|
||||
Output = output;
|
||||
Errors = errors;
|
||||
HasErrors = hasErrors;
|
||||
}
|
||||
|
||||
public Collection<PSObject> Output { get; }
|
||||
|
||||
public PSDataCollection<ErrorRecord> Errors { get; }
|
||||
|
||||
public bool HasErrors { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.CodeLenses
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents data expected back in an LSP CodeLens response.
|
||||
/// </summary>
|
||||
internal class CodeLensData
|
||||
{
|
||||
public string Uri { get; set; }
|
||||
|
||||
public string ProviderId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.CodeLenses
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the contract for a Code Lens provider.
|
||||
/// </summary>
|
||||
internal interface ICodeLensProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies a unique identifier for the feature provider, typically a
|
||||
/// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider"
|
||||
/// </summary>
|
||||
string ProviderId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provides a collection of CodeLenses for the given
|
||||
/// document.
|
||||
/// </summary>
|
||||
/// <param name="scriptFile">
|
||||
/// The document for which CodeLenses should be provided.
|
||||
/// </param>
|
||||
/// <returns>An IEnumerable of CodeLenses.</returns>
|
||||
IEnumerable<CodeLens> ProvideCodeLenses(ScriptFile scriptFile);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a CodeLens that was created without a Command.
|
||||
/// </summary>
|
||||
/// <param name="codeLens">
|
||||
/// The CodeLens to resolve.
|
||||
/// </param>
|
||||
/// <param name="scriptFile">
|
||||
/// The ScriptFile to resolve it in (sometimes unused).
|
||||
/// </param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>
|
||||
/// A Task which returns the resolved CodeLens when completed.
|
||||
/// </returns>
|
||||
Task<CodeLens> ResolveCodeLens(
|
||||
CodeLens codeLens,
|
||||
ScriptFile scriptFile,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerShell.EditorServices.Services;
|
||||
using Microsoft.PowerShell.EditorServices.Services.Symbols;
|
||||
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
|
||||
using OmniSharp.Extensions.LanguageServer.Protocol.Serialization;
|
||||
|
||||
namespace Microsoft.PowerShell.EditorServices.CodeLenses
|
||||
{
|
||||
internal class PesterCodeLensProvider : ICodeLensProvider
|
||||
{
|
||||
private readonly ConfigurationService _configurationService;
|
||||
|
||||
/// <summary>
|
||||
/// The symbol provider to get symbols from to build code lenses with.
|
||||
/// </summary>
|
||||
private readonly IDocumentSymbolProvider _symbolProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies a unique identifier for the feature provider, typically a
|
||||
/// fully-qualified name like "Microsoft.PowerShell.EditorServices.MyProvider"
|
||||
/// </summary>
|
||||
public string ProviderId => nameof(PesterCodeLensProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new Pester CodeLens provider for a given editor session.
|
||||
/// </summary>
|
||||
public PesterCodeLensProvider(ConfigurationService configurationService)
|
||||
{
|
||||
_configurationService = configurationService;
|
||||
_symbolProvider = new PesterDocumentSymbolProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the Pester CodeLenses for a given Pester symbol.
|
||||
/// </summary>
|
||||
/// <param name="pesterSymbol">The Pester symbol to get CodeLenses for.</param>
|
||||
/// <param name="scriptFile">The script file the Pester symbol comes from.</param>
|
||||
/// <returns>All CodeLenses for the given Pester symbol.</returns>
|
||||
private static CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, ScriptFile scriptFile)
|
||||
{
|
||||
string word = pesterSymbol.Command == PesterCommandType.It ? "test" : "tests";
|
||||
return new CodeLens[]
|
||||
{
|
||||
new CodeLens()
|
||||
{
|
||||
Range = pesterSymbol.ScriptRegion.ToRange(),
|
||||
Data = JToken.FromObject(new {
|
||||
Uri = scriptFile.DocumentUri,
|
||||
ProviderId = nameof(PesterCodeLensProvider)
|
||||
}, LspSerializer.Instance.JsonSerializer),
|
||||
Command = new Command()
|
||||
{
|
||||
Name = "PowerShell.RunPesterTests",
|
||||
Title = $"Run {word}",
|
||||
Arguments = JArray.FromObject(new object[]
|
||||
{
|
||||
scriptFile.DocumentUri,
|
||||
false /* No debug */,
|
||||
pesterSymbol.TestName,
|
||||
pesterSymbol.ScriptRegion?.StartLineNumber
|
||||
}, LspSerializer.Instance.JsonSerializer)
|
||||
}
|
||||
},
|
||||
|
||||
new CodeLens()
|
||||
{
|
||||
Range = pesterSymbol.ScriptRegion.ToRange(),
|
||||
Data = JToken.FromObject(new {
|
||||
Uri = scriptFile.DocumentUri,
|
||||
ProviderId = nameof(PesterCodeLensProvider)
|
||||
}, LspSerializer.Instance.JsonSerializer),
|
||||
Command = new Command()
|
||||
{
|
||||
Name = "PowerShell.RunPesterTests",
|
||||
Title = $"Debug {word}",
|
||||
Arguments = JArray.FromObject(new object[]
|
||||
{
|
||||
scriptFile.DocumentUri,
|
||||
true /* No debug */,
|
||||
pesterSymbol.TestName,
|
||||
pesterSymbol.ScriptRegion?.StartLineNumber
|
||||
},
|
||||
LspSerializer.Instance.JsonSerializer)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all Pester CodeLenses for a given script file.
|
||||
/// </summary>
|
||||
/// <param name="scriptFile">The script file to get Pester CodeLenses for.</param>
|
||||
/// <returns>All Pester CodeLenses for the given script file.</returns>
|
||||
public IEnumerable<CodeLens> ProvideCodeLenses(ScriptFile scriptFile)
|
||||
{
|
||||
// Don't return anything if codelens setting is disabled
|
||||
if (!_configurationService.CurrentSettings.Pester.CodeLens)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile))
|
||||
{
|
||||
if (symbol is not PesterSymbolReference pesterSymbol)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip CodeLens for setup/teardown block
|
||||
if (!PesterSymbolReference.IsPesterTestCommand(pesterSymbol.Command))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_configurationService.CurrentSettings.Pester.UseLegacyCodeLens
|
||||
&& pesterSymbol.Command != PesterCommandType.Describe)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (CodeLens codeLens in GetPesterLens(pesterSymbol, scriptFile))
|
||||
{
|
||||
yield return codeLens;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the CodeLens provision asynchronously -- just wraps the CodeLens argument in a task.
|
||||
/// </summary>
|
||||
/// <param name="codeLens">The code lens to resolve.</param>
|
||||
/// <param name="scriptFile">The script file.</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>The given CodeLens, wrapped in a task.</returns>
|
||||
public Task<CodeLens> ResolveCodeLens(CodeLens codeLens, ScriptFile scriptFile, CancellationToken cancellationToken) =>
|
||||
// This provider has no specific behavior for
|
||||
// resolving CodeLenses.
|
||||
Task.FromResult(codeLens);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue