emacs/code/elpa/lsp-mode-20240319.1043/lsp-terraform.el

458 lines
19 KiB
EmacsLisp
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; lsp-terraform.el --- Terraform Client settings -*- lexical-binding: t; -*-
;; Copyright (C) 2019 Ross Donaldson, Sibi Prabakaran
;; Author: Ross Donaldson, Sibi Prabakaran
;; Keywords: terraform 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;; LSP client for Terraform
;;; Code:
(require 'lsp-mode)
(require 'lsp-semantic-tokens)
(require 'lsp-protocol)
(require 'dash)
;; terraform-lsp
(defgroup lsp-terraform nil
"LSP support for Terraform, using terraform-lsp."
:group 'lsp-mode
:link '(url-link "https://github.com/juliosueiras/terraform-lsp")
:package-version `(lsp-mode . "6.2"))
(defcustom lsp-terraform-server "terraform-lsp"
"Path to the `terraform-lsp' binary."
:group 'lsp-terraform
:risky t
:type '(choice
(file :tag "File")
(repeat string))
:package-version `(lsp-mode . "6.2"))
(defcustom lsp-terraform-enable-logging nil
"If non-nil, enable `terraform-ls''s native logging."
:group 'lsp-terraform
:risky t
:type 'boolean
:package-version `(lsp-mode . "6.2"))
(defun lsp-terraform--make-launch-cmd ()
(-let [base (if (stringp lsp-terraform-server)
`(,lsp-terraform-server)
lsp-terraform-server)]
(when lsp-terraform-enable-logging
(push "-enable-log-file" base))
base))
(lsp-register-client
(make-lsp-client :new-connection (lsp-stdio-connection #'lsp-terraform--make-launch-cmd)
:major-modes '(terraform-mode)
:priority -1
:server-id 'tfls))
;; terraform-ls
(defgroup lsp-terraform-ls nil
"LSP support for Terraform, using terraform-ls from Hashicorp."
:group 'lsp-mode
:link '(url-link "https://github.com/hashicorp/terraform-ls")
:package-version `(lsp-mode . "8.0.1"))
(defcustom lsp-terraform-ls-server "terraform-ls"
"Path to the `terraform-ls' binary."
:group 'lsp-terraform-ls
:risky t
:type '(choice
(file :tag "File")
(repeat string))
:package-version `(lsp-mode . "8.0.1"))
(defcustom lsp-terraform-ls-enable-show-reference nil
"Enable reference counts.
Display reference counts above top level blocks and
attributes. This is an experimental feature provided by the
language server."
:group 'lsp-terraform-ls
:type 'boolean
:package-version '(lsp-mode . "8.0.1"))
(defcustom lsp-terraform-ls-validate-on-save nil
"Enable validating the current open file on save.
This is an experimental feature provided by the language server."
:group 'lsp-terraform-ls
:type 'boolean
:package-version '(lsp-mode . "8.0.1"))
(defcustom lsp-terraform-ls-prefill-required-fields nil
"Enable completion of required fields.
Enable autocompletion for required fields when completing
Terraform blocks. This is an experimental feature provided by the
language server."
:group 'lsp-terraform-ls
:type 'boolean
:package-version '(lsp-mode . "8.0.1"))
(defcustom lsp-terraform-ls-providers-position-params nil
"The optional providers tree position params.
Defaults to side following treemacs default."
:type 'alist
:group 'lsp-terraform-ls
:package-version '(lsp-mode . "8.0.1"))
(defcustom lsp-terraform-ls-module-calls-position-params nil
"The optional module calls tree position params.
Defaults to side following treemacs default."
:type 'alist
:group 'lsp-terraform-ls
:package-version '(lsp-mode . "8.0.1"))
(defun lsp-terraform-ls--make-launch-cmd ()
`(,lsp-terraform-ls-server "serve"))
(lsp-defun lsp-terraform-ls--show-references ((&Command :arguments?))
"Show references for command with ARGS."
(lsp-show-xrefs
(lsp--locations-to-xref-items
(lsp-request "textDocument/references"
(lsp--make-reference-params
(lsp--text-document-position-params nil (elt arguments? 0)))))
t
t))
(defun lsp-terraform-ls--custom-capabilities ()
"Construct custom capabilities for the language server."
(when lsp-terraform-ls-enable-show-reference
'((experimental . ((showReferencesCommandId . "client.showReferences"))))))
(defun lsp-terraform-ls--init-options ()
"Construct initialization options for the lanague server."
`((experimentalFeatures . ((validateOnSave . ,(lsp-json-bool lsp-terraform-ls-validate-on-save))
(prefillRequiredFields . ,(lsp-json-bool lsp-terraform-ls-prefill-required-fields))))))
(defcustom lsp-terraform-semantic-token-faces
'(("namespace" . lsp-face-semhl-namespace)
("type" . lsp-face-semhl-type)
("class" . lsp-face-semhl-class)
("enum" . lsp-face-semhl-enum)
("interface" . lsp-face-semhl-interface)
("struct" . lsp-face-semhl-struct)
("typeParameter" . lsp-face-semhl-type-parameter)
("parameter" . lsp-face-semhl-parameter)
("variable" . lsp-face-semhl-variable)
("property" . lsp-face-semhl-property)
("enumMember" . lsp-face-semhl-constant)
("event" . lsp-face-semhl-event)
("function" . lsp-face-semhl-function)
("method" . lsp-face-semhl-method)
("macro" . lsp-face-semhl-macro)
("keyword" . lsp-face-semhl-keyword)
("modifier" . lsp-face-semhl-member)
("comment" . lsp-face-semhl-comment)
("string" . lsp-face-semhl-string)
("number" . lsp-face-semhl-number)
("regexp" . lsp-face-semhl-regexp)
("operator" . lsp-face-semhl-operator)
("hcl-attrName" . lsp-face-semhl-member)
("hcl-blockType" . lsp-face-semhl-struct)
("hcl-blockLabel" . lsp-face-semhl-member)
("hcl-bool" . lsp-face-semhl-constant)
("hcl-string" . lsp-face-semhl-string)
("hcl-number" . lsp-face-semhl-number)
("hcl-objectKey" . lsp-face-semhl-member)
("hcl-mapKey" . lsp-face-semhl-member)
("hcl-keyword" . lsp-face-semhl-keyword)
("hcl-traversalStep" . lsp-face-semhl-member)
("hcl-typeCapsule" . lsp-face-semhl-type)
("hcl-typePrimitive" . lsp-face-semhl-type))
"Mapping between terrafom-ls tokens and fonts to apply."
:group 'lsp-terraform
:type '(alist :key-type string :value-type face)
:package-version '(lsp-mode . "8.1"))
(defcustom lsp-terraform-semantic-token-modifier-faces
'(("declaration" . lsp-face-semhl-class)
("definition" . lsp-face-semhl-definition)
("readonly" . lsp-face-semhl-constant)
("static" . lsp-face-semhl-static)
("deprecated" . lsp-face-semhl-deprecated)
("abstract" . lsp-face-semhl-keyword)
("async" . lsp-face-semhl-macro)
("modification" . lsp-face-semhl-operator)
("documentation" . lsp-face-semhl-comment)
("defaultLibrary" . lsp-face-semhl-default-library)
("hcl-dependent" . lsp-face-semhl-constant)
("terraform-data" . lsp-face-semhl-constant)
("terraform-locals" . lsp-face-semhl-variable)
("terraform-module" . lsp-face-semhl-namespace)
("terraform-output" . lsp-face-semhl-constant)
("terraform-provider" . lsp-face-semhl-class)
("terraform-resource" . lsp-face-semhl-interface)
("terraform-provisioner" . lsp-face-semhl-default-library)
("terraform-connection" . lsp-face-semhl-constant)
("terraform-variable" . lsp-face-semhl-variable)
("terraform-terraform" . lsp-face-semhl-constant)
("terraform-backend" . lsp-face-semhl-definition)
("terraform-name" . lsp-face-semhl-interface)
("terraform-type" . lsp-face-semhl-type)
("terraform-requiredProviders" . lsp-face-semhl-default-library))
"Mapping between terraform-ls modifiers and fonts to apply."
:group 'lsp-terraform
:type '(alist :key-type string :value-type face)
:package-version '(lsp-mode . "8.1"))
(lsp-register-client
(make-lsp-client :new-connection (lsp-stdio-connection #'lsp-terraform-ls--make-launch-cmd)
:major-modes '(terraform-mode)
:priority 1
:server-id 'tfmls
:action-handlers (ht ("client.showReferences" #'lsp-terraform-ls--show-references))
:semantic-tokens-faces-overrides `(:discard-default-modifiers t
:discard-default-types t
:modifiers ,lsp-terraform-semantic-token-modifier-faces
:types ,lsp-terraform-semantic-token-faces)
:initialization-options (lsp-terraform-ls--init-options)
:custom-capabilities (lsp-terraform-ls--custom-capabilities)))
(defun lsp-terraform-ls-validate ()
"Execute terraform validate on project root."
(interactive)
(lsp-request
"workspace/executeCommand"
(list :command "terraform-ls.terraform.validate"
:arguments (vector (format "uri=%s" (lsp--path-to-uri (lsp-workspace-root))))
)
:no-wait t
:no-merge t))
(defun lsp-terraform-ls-init ()
"Execute terraform init on project root.
This is a synchronous action."
(interactive)
(lsp-request
"workspace/executeCommand"
(list :command "terraform-ls.terraform.init"
:arguments (vector (format "uri=%s" (lsp--path-to-uri (lsp-workspace-root)))))
:no-wait nil
:no-merge t))
(defun lsp-terraform-ls-version ()
"Get information about the terraform binary version for the current module."
(interactive)
(let ((terraform-data (lsp-request
"workspace/executeCommand"
(list :command "terraform-ls.module.terraform"
:arguments (vector (format "uri=%s" (lsp--path-to-uri (lsp-workspace-root))))))))
(lsp--info "Required: %s, Current: %s"
(lsp:terraform-ls-module-terraform-required-version terraform-data)
(lsp:terraform-ls-module-terraform-discovered-version terraform-data))))
(lsp-consistency-check lsp-terraform)
(defvar treemacs-position)
(defvar treemacs-width)
(declare-function lsp-treemacs-render "ext:lsp-treemacs" (tree title expand-depth &optional buffer-name))
(defvar-local lsp-terraform-ls--providers-tree-data nil)
(defvar-local lsp-terraform-ls--modules-call-tree-data nil)
(defvar-local lsp-tf--modules-control-buffer nil)
(defconst lsp-terraform-ls--providers-buffer-name "*Terraform Providers*")
(defconst lsp-terraform-ls--modules-buffer-name "*Terraform Modules*")
(defvar lsp-terraform-modules-mode-map
(let ((m (make-sparse-keymap)))
(define-key m (kbd "g") 'lsp-terraform-ls--modules-refresh)
m)
"Keymap for `lsp-terraform-modules-mode'.")
(define-minor-mode lsp-terraform-modules-mode "LSP Treemacs mode for terraform modules."
:keymap lsp-terraform-modules-mode-map
:group 'lsp-terraform-ls)
(cl-defstruct tf-package display-name doc-link installed-version version-constraint)
(cl-defstruct tf-module name doc-link version source-type dependent-modules)
(defun construct-tf-package (provider installed-version)
"Construct `TF-PACKAGE' using PROVIDER and INSTALLED-VERSION."
(make-tf-package :display-name (lsp-get provider :display_name)
:doc-link (lsp-get provider :docs_link)
:installed-version installed-version
:version-constraint (lsp-get provider :version_constraint)))
(lsp-defun construct-tf-module ((&terraform-ls:Module :name :docs-link :version :source-type :dependent-modules))
"Construct `TF-MODULE' using MODULE."
(make-tf-module :name name
:doc-link docs-link
:version version
:source-type source-type
:dependent-modules dependent-modules))
(lsp-defun lsp-terraform-ls--providers-to-tf-package ((&terraform-ls:Providers :provider-requirements :installed-providers))
"Convert PROVIDERS-TREE-DATA to list of `tf-package'."
(let* ((provider-requirements-keys (hash-table-keys provider-requirements))
(installed-versions (mapcar (lambda (x) (lsp-get installed-providers (make-symbol (format ":%s" x)))) provider-requirements-keys))
(providers (mapcar (lambda (x) (lsp-get provider-requirements (make-symbol (format ":%s" x)))) provider-requirements-keys))
(tf-packages (-zip-with (lambda (x y) (construct-tf-package x y)) providers installed-versions)))
tf-packages))
(lsp-defun lsp-terraform-ls--modules-to-tf-module ((&terraform-ls:ModuleCalls :module-calls))
"Convert MODULES-TREE-DATA to list of `TF-MODULE'."
(let* ((modules (-map (lambda (x) (construct-tf-module x)) module-calls)))
modules))
(defun lsp-terraform-ls--fetch-modules-data (project-root)
"Fetch modules data and set it in `lsp-terraform-ls--modules-call-tree-data'."
(let* ((tree-data (lsp-request
"workspace/executeCommand"
(list :command "terraform-ls.module.calls"
:arguments (vector (format "uri=%s" (lsp--path-to-uri project-root))))
:no-wait nil
:no-merge nil))
(modules (lsp-terraform-ls--modules-to-tf-module tree-data)))
(setq-local lsp-terraform-ls--modules-call-tree-data modules)))
(defun lsp-terraform-ls--fetch-providers ()
"Fetch modules call data and set it in `lsp-terraform-ls--providers-tree-data'."
(let* ((tree-data (lsp-request
"workspace/executeCommand"
(list :command "terraform-ls.module.providers"
:arguments (vector (format "uri=%s" (lsp--path-to-uri (lsp-workspace-root)))))
:no-wait nil
:no-merge nil))
(tf-packages (lsp-terraform-ls--providers-to-tf-package tree-data)))
(setq-local lsp-terraform-ls--providers-tree-data tf-packages)))
(defun lsp-terraform-ls--tf-packages-to-treemacs (tf-packages)
"Convert list of `TF-PACKAGES' to treemacs compatible data."
(mapcar (lambda (package) (list :label (format "%s %s" (tf-package-display-name package) (tf-package-installed-version package))
:icon 'package
:key (tf-package-display-name package)
:children (list (list
:icon 'library
:label (tf-package-version-constraint package)))
:ret-action (lambda (&rest _) (browse-url (tf-package-doc-link package))))) tf-packages))
(defun lsp-terraform-ls--tf-modules-to-treemacs (tf-modules)
"Convert list of `TF-MODULES' to treemacs compatible data."
(mapcar (lambda (module) (list :label (format "%s %s" (tf-module-name module) (tf-module-version module))
:icon 'package
:key (tf-module-name module)
:ret-action (lambda (&rest _) (browse-url (tf-module-doc-link module)))
)) tf-modules))
(defun lsp-terraform-ls--show-providers (ignore-focus?)
"Show terraform providers and focus on it if IGNORE-FOCUS? is nil."
(unless lsp-terraform-ls--providers-tree-data
(lsp-terraform-ls--fetch-providers))
(let* ((lsp-terraform-treemacs
(lsp-terraform-ls--tf-packages-to-treemacs lsp-terraform-ls--providers-tree-data))
(buffer (lsp-treemacs-render lsp-terraform-treemacs
lsp-terraform-ls--providers-buffer-name
t
"Terraform Providers"))
(position-params (or lsp-terraform-ls-providers-position-params
`((side . ,treemacs-position)
(slot . 2)
(window-width . ,treemacs-width))))
(window
(display-buffer-in-side-window buffer position-params)))
(unless ignore-focus?
(select-window window)
(set-window-dedicated-p window t))))
(defun lsp-terraform-ls--show-module-calls (ignore-focus? project-root)
"Show terraform modules and focus on it if IGNORE-FOCUS? is nil."
(unless lsp-terraform-ls--modules-call-tree-data
(lsp-terraform-ls--fetch-modules-data project-root))
(unless lsp-terraform-ls--modules-call-tree-data
(error "Modules data is empty"))
(let* ((lsp-terraform-treemacs
(lsp-terraform-ls--tf-modules-to-treemacs lsp-terraform-ls--modules-call-tree-data))
(buffer (lsp-treemacs-render lsp-terraform-treemacs
lsp-terraform-ls--modules-buffer-name
t
"Terraform Modules"))
(modules-buffer (current-buffer))
(position-params (or lsp-terraform-ls-module-calls-position-params
`((side . ,treemacs-position)
(slot . 1)
(window-width . ,treemacs-width))))
(window
(display-buffer-in-side-window buffer position-params)))
(select-window window)
(setq-local lsp-tf--modules-control-buffer modules-buffer)
(lsp-terraform-modules-mode t)
(set-window-dedicated-p window t)
(when ignore-focus?
(select-window (previous-window)))))
(defun lsp-terraform-ls--refresh-module-calls ()
"Refresh terraform modules."
(lsp-terraform-ls--fetch-modules-data (lsp-workspace-root))
(unless lsp-terraform-ls--modules-call-tree-data
(error "Modules data is empty"))
(let* ((lsp-terraform-treemacs
(lsp-terraform-ls--tf-modules-to-treemacs lsp-terraform-ls--modules-call-tree-data))
(buffer (lsp-treemacs-render lsp-terraform-treemacs
lsp-terraform-ls--modules-buffer-name
t
"Terraform Modules"))
(position-params (or lsp-terraform-ls-module-calls-position-params
`((side . ,treemacs-position)
(slot . 1)
(window-width . ,treemacs-width))))
(window
(display-buffer-in-side-window buffer position-params)))
(select-window window)
(lsp-terraform-modules-mode t)
(set-window-dedicated-p window t)
(lsp--info "Refresh completed")))
(defun lsp-terraform-ls-providers (&optional ignore-focus?)
"Show terraform providers with focus on it if IGNORE-FOCUS? is nil."
(interactive)
(if (require 'lsp-treemacs nil t)
(lsp-terraform-ls--show-providers ignore-focus?)
(error "The package lsp-treemacs is not installed")))
(defun lsp-terraform-ls-module-calls (&optional ignore-focus?)
"Show terraform modules with focus on it if IGNORE-FOCUS? is nil."
(interactive)
(if (require 'lsp-treemacs nil t)
(lsp-terraform-ls--show-module-calls ignore-focus? (lsp-workspace-root))
(error "The package lsp-treemacs is not installed")))
(defun lsp-terraform-ls--modules-refresh ()
"Refresh terraform modules data."
(interactive)
(unless (buffer-live-p lsp-tf--modules-control-buffer)
(error "Original buffer not present. Do M-x lsp-terraform-ls-module-calls"))
(with-current-buffer lsp-tf--modules-control-buffer
(lsp-terraform-ls--refresh-module-calls)))
(provide 'lsp-terraform)
;;; lsp-terraform.el ends here