;;; lsp-pwsh.el --- client for PowerShellEditorServices -*- lexical-binding: t; -*- ;; Copyright (C) 2019 Kien Nguyen ;; Author: kien.n.quang at gmail.com ;; Keywords: lsp ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; ;;; Code: (require 'f) (require 'dash) (require 's) (require 'ht) (require 'lsp-protocol) (require 'lsp-mode) (defgroup lsp-pwsh nil "LSP support for PowerShell, using the PowerShellEditorServices." :group 'lsp-mode :package-version '(lsp-mode . "6.2")) ;; PowerShell vscode flags (defcustom lsp-pwsh-help-completion "BlockComment" "Controls the comment-based help completion behavior triggered by typing '##'. Set the generated help style with 'BlockComment' or 'LineComment'. Disable the feature with 'Disabled'." :type '(choice (:tag "Disabled" "BlockComment" "LineComment")) :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-script-analysis-enable t "Enables real-time script analysis from PowerShell Script Analyzer. Uses the newest installed version of the PSScriptAnalyzer module or the version bundled with this extension, if it is newer." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-script-analysis-settings-path "" "Specifies the path to a PowerShell Script Analyzer settings file. To override the default settings for all projects, enter an absolute path, or enter a path relative to your workspace." :type 'string :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-folding-enable t "Enables syntax based code folding. When disabled, the default indentation based code folding is used." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-folding-show-last-line t "Shows the last line of a folded section. Similar to the default VSCode folding style. When disabled, the entire folded region is hidden." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-preset "Custom" "Sets the codeformatting options to follow the given indent style. Sets in a way that is compatible with PowerShell syntax. For more information about the brace styles please refer to https://github.com/PoshCode/PowerShellPracticeAndStyle/issues/81." :type '(choice (:tag "Custom" "Allman" "OTBS" "Stroustrup")) :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-open-brace-on-same-line t "Places open brace on the same line as its associated statement." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-new-line-after-open-brace t "Adds a newline (line break) after an open brace." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-new-line-after-close-brace t "Adds a newline (line break) after a closing brace." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-pipeline-indentation-style "NoIndentation" "Multi-line pipeline style settings." :type '(choice (:tag "IncreaseIndentationForFirstPipeline" "IncreaseIndentationAfterEveryPipeline" "NoIndentation")) :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-whitespace-before-open-brace t "Adds a space between a keyword and its associated scriptblock expression." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-whitespace-before-open-paren t "Adds a space between a keyword (if, elseif, while, switch, etc) and its associated conditional expression." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-whitespace-around-operator t "Adds spaces before and after an operator ('=', '+', '-', etc.)." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-whitespace-after-separator t "Adds a space after a separator (',' and ';')." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-whitespace-inside-brace t "Adds a space after an opening brace ('{') and before a closing brace ('}')." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-whitespace-around-pipe t "Adds a space before and after the pipeline operator ('|')." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-ignore-one-line-block t "Does not reformat one-line code blocks, such as \"if (...) {...} else {...}\"." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-align-property-value-pairs t "Align assignment statements in a hashtable or a DSC Configuration." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-code-formatting-use-correct-casing nil "Use correct casing for cmdlets." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-developer-editor-services-log-level "Normal" "Sets the log level for the PowerShell Editor Services host executable. Valid values are 'Diagnostic', 'Verbose', 'Normal', 'Warning', and 'Error'" :type '(choice (:tag "Diagnostic" "Verbose" "Normal" "Warning" "Error")) :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-developer-editor-services-wait-for-debugger nil "Launches the language service with the /waitForDebugger flag to force it to wait for a .NET debugger to attach before proceeding." :type 'boolean :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-developer-feature-flags nil "An array of strings that enable experimental features in the PowerShell extension." :type '(repeat string) :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (lsp-register-custom-settings '(("powershell.developer.featureFlags" lsp-pwsh-developer-feature-flags) ("powershell.developer.editorServicesWaitForDebugger" lsp-pwsh-developer-editor-services-wait-for-debugger t) ("powershell.codeFormatting.useCorrectCasing" lsp-pwsh-code-formatting-use-correct-casing t) ("powershell.codeFormatting.alignPropertyValuePairs" lsp-pwsh-code-formatting-align-property-value-pairs t) ("powershell.codeFormatting.ignoreOneLineBlock" lsp-pwsh-code-formatting-ignore-one-line-block t) ("powershell.codeFormatting.whitespaceAroundPipe" lsp-pwsh-code-formatting-whitespace-around-pipe t) ("powershell.codeFormatting.whitespaceInsideBrace" lsp-pwsh-code-formatting-whitespace-inside-brace t) ("powershell.codeFormatting.whitespaceAfterSeparator" lsp-pwsh-code-formatting-whitespace-after-separator t) ("powershell.codeFormatting.whitespaceAroundOperator" lsp-pwsh-code-formatting-whitespace-around-operator t) ("powershell.codeFormatting.whitespaceBeforeOpenParen" lsp-pwsh-code-formatting-whitespace-before-open-paren t) ("powershell.codeFormatting.whitespaceBeforeOpenBrace" lsp-pwsh-code-formatting-whitespace-before-open-brace t) ("powershell.codeFormatting.pipelineIndentationStyle" lsp-pwsh-code-formatting-pipeline-indentation-style) ("powershell.codeFormatting.newLineAfterCloseBrace" lsp-pwsh-code-formatting-new-line-after-close-brace t) ("powershell.codeFormatting.newLineAfterOpenBrace" lsp-pwsh-code-formatting-new-line-after-open-brace t) ("powershell.codeFormatting.openBraceOnSameLine" lsp-pwsh-code-formatting-open-brace-on-same-line t) ("powershell.codeFormatting.preset" lsp-pwsh-code-formatting-preset) ("powershell.codeFolding.showLastLine" lsp-pwsh-code-folding-show-last-line t) ("powershell.codeFolding.enable" lsp-pwsh-code-folding-enable t) ("powershell.scriptAnalysis.settingsPath" lsp-pwsh-script-analysis-settings-path) ("powershell.scriptAnalysis.enable" lsp-pwsh-script-analysis-enable t) ("powershell.helpCompletion" lsp-pwsh-help-completion))) ;; lsp-pwsh custom variables (defcustom lsp-pwsh-ext-path (expand-file-name "pwsh" lsp-server-install-dir) "The path to powershell vscode extension." :type 'string :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-exe (or (executable-find "pwsh") (executable-find "powershell")) "PowerShell executable." :type 'string :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defcustom lsp-pwsh-dir lsp-pwsh-ext-path "Path to PowerShellEditorServices without last slash." :type 'string :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defvar lsp-pwsh-pses-script (expand-file-name "PowerShellEditorServices/Start-EditorServices.ps1" lsp-pwsh-dir) "Main script to start PSES.") (defvar lsp-pwsh-log-path (expand-file-name "logs" lsp-pwsh-ext-path) "Path to directory where server will write log files. Must not nil.") (defvar lsp-pwsh--sess-id (emacs-pid)) (defun lsp-pwsh--command () "Return the command to start server." `(,lsp-pwsh-exe "-NoProfile" "-NonInteractive" "-NoLogo" ,@(if (eq system-type 'windows-nt) '("-ExecutionPolicy" "Bypass")) "-OutputFormat" "Text" "-File" ,lsp-pwsh-pses-script "-HostName" "\"Emacs Host\"" "-HostProfileId" "'Emacs.LSP'" "-HostVersion" "8.0.1" "-LogPath" ,(expand-file-name "emacs-powershell.log" lsp-pwsh-log-path) "-LogLevel" ,lsp-pwsh-developer-editor-services-log-level "-SessionDetailsPath" ,(expand-file-name (format "PSES-VSCode-%d" lsp-pwsh--sess-id) lsp-pwsh-log-path) ;; "-AdditionalModules" "@('PowerShellEditorServices.VSCode')" "-Stdio" "-BundledModulesPath" ,lsp-pwsh-dir "-FeatureFlags" "@()")) (defun lsp-pwsh--extra-init-params () "Return form describing parameters for language server.") (lsp-defun lsp-pwsh--apply-code-action-edits ((&Command :command :arguments?)) "Handle ACTION for PowerShell.ApplyCodeActionEdits." (-if-let* (((&pwsh:ScriptRegion :start-line-number :end-line-number :start-column-number :end-column-number :text) (lsp-seq-first arguments?)) (start-position (lsp-make-position :line (1- start-line-number) :character (1- start-column-number))) (end-position (lsp-make-position :line (1- end-line-number) :character (1- end-column-number))) (edits `[,(lsp-make-text-edit :range (lsp-make-range :start start-position :end end-position) :newText text)])) (lsp--apply-text-edits edits 'code-action) (lsp-send-execute-command command arguments?))) (lsp-defun lsp-pwsh--show-code-action-document ((&Command :arguments?)) "Handle ACTION for PowerShell.ShowCodeActionDocumentation." (-if-let* ((rule-raw (lsp-seq-first arguments?)) (rule-id (if (s-prefix-p "PS" rule-raw) (substring rule-raw 2) rule-raw))) (browse-url (concat "https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/" rule-id)) (lsp-warn "Cannot show documentation for code action, no ruleName was supplied"))) (defvar lsp-pwsh--major-modes '(powershell-mode)) (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection #'lsp-pwsh--command (lambda () (f-exists? lsp-pwsh-pses-script))) :major-modes lsp-pwsh--major-modes :server-id 'pwsh-ls :priority -1 :initialization-options #'lsp-pwsh--extra-init-params :notification-handlers (ht ("powerShell/executionStatusChanged" #'ignore) ("output" #'ignore)) :action-handlers (ht ("PowerShell.ApplyCodeActionEdits" #'lsp-pwsh--apply-code-action-edits) ("PowerShell.ShowCodeActionDocumentation" #'lsp-pwsh--show-code-action-document)) :initialized-fn (lambda (w) (with-lsp-workspace w (lsp--set-configuration (lsp-configuration-section "powershell"))) (let ((caps (lsp--workspace-server-capabilities w))) (lsp:set-server-capabilities-document-range-formatting-provider? caps t) (lsp:set-server-capabilities-document-formatting-provider? caps t))) :download-server-fn #'lsp-pwsh-setup)) (defcustom lsp-pwsh-github-asset-url "https://github.com/%s/%s/releases/latest/download/%s" "GitHub latest asset template url." :type 'string :group 'lsp-pwsh :package-version '(lsp-mode . "6.2")) (defun lsp-pwsh-setup (_client callback error-callback update) "Downloads PowerShellEditorServices to `lsp-pwsh-dir'. CALLBACK is called when the download finish successfully otherwise ERROR-CALLBACK is called. UPDATE is non-nil if it is already downloaded. FORCED if specified with prefix argument." (unless (and lsp-pwsh-exe (file-executable-p lsp-pwsh-exe)) (user-error "Use `lsp-pwsh-exe' with the value of `%s' is not a valid powershell binary" lsp-pwsh-exe)) (let ((url (format lsp-pwsh-github-asset-url "PowerShell" "PowerShellEditorServices" "PowerShellEditorServices.zip")) (temp-file (make-temp-file "ext" nil ".zip"))) (unless (f-exists? lsp-pwsh-log-path) (mkdir lsp-pwsh-log-path 'create-parent)) (unless (and (not update) (f-exists? lsp-pwsh-pses-script)) ;; since we know it's installed, use powershell to download the file ;; (and avoid url.el bugginess or additional libraries) (when (f-exists? lsp-pwsh-dir) (delete-directory lsp-pwsh-dir 'recursive)) (lsp-async-start-process callback error-callback lsp-pwsh-exe "-noprofile" "-noninteractive" "-nologo" "-ex" "bypass" "-command" "Invoke-WebRequest" "-UseBasicParsing" "-uri" url "-outfile" temp-file ";" "Expand-Archive" "-Path" temp-file "-DestinationPath" lsp-pwsh-dir)))) (lsp-consistency-check lsp-pwsh) (provide 'lsp-pwsh) ;;; lsp-pwsh.el ends here