;;; lsp-yaml.el --- LSP YAML server integration        -*- lexical-binding: t; -*-

;; Copyright (C) 2019  Aya Igarashi
;; Copyright (C) 2019-2026 lsp-mode maintainers

;; Author: Aya Igarashi <ladiclexxx@gmail.com>
;; Keywords:

;; 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:

;;

;;; Code:

(require 'lsp-mode)
(require 'dash)

(defgroup lsp-yaml nil
  "LSP support for YAML, using yaml-language-server."
  :group 'lsp-mode
  :link '(url-link "https://github.com/redhat-developer/yaml-language-server")
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-format-enable t
  "Enable/disable default YAML formatter."
  :type 'boolean
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-single-quote nil
  "Use single quote instead of double quotes."
  :type 'boolean
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-bracket-spacing t
  "Print spaces between brackets in objects."
  :type 'boolean
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-prose-wrap "preserve"
  "Options for prose-wrap.
   Always: wrap prose if it exceeds the print width.
   Never: never wrap the prose.
   Preserve: wrap prose as-is."
  :type '(choice (const "always")
                 (const "never")
                 (const "preserve"))
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-print-width 80
  "Specify the line length that the printer will wrap on."
  :type 'number
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-validate t
  "Enable/disable validation feature."
  :type 'boolean
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-hover t
  "Enable/disable hover feature."
  :type 'boolean
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-completion t
  "Enable/disable completion feature."
  :type 'boolean
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-schemas '()
  "Associate schemas to YAML files in a glob pattern."
  :type '(alist :key-type (symbol :tag "schema") :value-type (lsp-string-vector :tag "files (glob)"))
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-schema-store-enable t
  "Enable/disable JSON Schema store. When set to true, available YAML
   schemas will be automatically pulled from the store."
  :type 'boolean
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-custom-tags nil
  "Custom tags for the parser to use."
  :type '(lsp-repeatable-vector string)
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(defcustom lsp-yaml-schema-store-uri "https://www.schemastore.org/api/json/catalog.json"
  "URL of schema store catalog to use."
  :type 'string
  :group 'lsp-yaml)

(defcustom lsp-yaml-schema-store-local-db
  (expand-file-name
   (locate-user-emacs-file (f-join ".cache" "lsp" "lsp-yaml-schemas.json")))
  "Cached database of schema store."
  :type 'file
  :group 'lsp-yaml)

(defcustom lsp-yaml-max-items-computed 5000
  "The maximum number of outline symbols and folding regions computed.
Limited for performance reasons."
  :type 'number
  :group 'lsp-yaml
  :package-version '(lsp-mode . "8.0.0"))


(defvar lsp-yaml--schema-store-schemas-alist nil
  "A list of schemas fetched from schema stores.")

(lsp-register-custom-settings
 '(("yaml.format.enable" lsp-yaml-format-enable t)
   ("yaml.format.singleQuote" lsp-yaml-single-quote t)
   ("yaml.format.bracketSpacing" lsp-yaml-bracket-spacing)
   ("yaml.format.proseWrap" lsp-yaml-prose-wrap)
   ("yaml.format.printWidth" lsp-yaml-print-width)
   ("yaml.validate" lsp-yaml-validate t)
   ("yaml.hover" lsp-yaml-hover t)
   ("yaml.completion" lsp-yaml-completion t)
   ("yaml.schemas" lsp-yaml-schemas)
   ("yaml.schemaStore.enable" lsp-yaml-schema-store-enable t)
   ("yaml.schemaStore.url" lsp-yaml-schema-store-uri)
   ("yaml.customTags" lsp-yaml-custom-tags)
   ("yaml.maxItemsComputed" lsp-yaml-max-items-computed)))

(defcustom lsp-yaml-server-command '("yaml-language-server" "--stdio")
  "Command to start yaml-languageserver."
  :type '(repeat string)
  :group 'lsp-yaml
  :package-version '(lsp-mode . "6.2"))

(lsp-dependency 'yaml-language-server
                '(:system "yaml-language-server")
                '(:npm :package "yaml-language-server"
                       :path "yaml-language-server"))

(lsp-register-client
 (make-lsp-client :new-connection (lsp-stdio-connection
                                   (lambda ()
                                     `(,(or (executable-find (cl-first lsp-yaml-server-command))
                                            (lsp-package-path 'yaml-language-server))
                                       ,@(cl-rest lsp-yaml-server-command))))
                  :activation-fn (lsp-activate-on "yaml")
                  :priority 0
                  :server-id 'yamlls
                  :initialized-fn (lambda (workspace)
                                    (with-lsp-workspace workspace
                                      (lsp--set-configuration
                                       (lsp-configuration-section "yaml"))))
                  :download-server-fn (lambda (_client callback error-callback _update?)
                                        (lsp-package-ensure 'yaml-language-server
                                                            callback error-callback))))

(defconst lsp--yaml-schema-extension-type
  '(list (cons 'name  string)
         (cons 'description string)
         (cons 'url string)
         (cons 'fileMatch (repeat string))))

(defcustom lsp-yaml-schema-extensions '(((name . "Kubernetes v1.30.3")
                                         (description . "Kubernetes v1.30.3 manifest schema definition")
                                         (url . "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.30.3-standalone-strict/all.json")
                                         (fileMatch . ["*-k8s.yaml" "*-k8s.yml"])))
  "User defined schemas that extend default schema store.
Used in `lsp-yaml--get-supported-schemas' to supplement schemas provided by
`lsp-yaml-schema-store-uri'."
  :type  '(repeat lsp--yaml-schema-extension-type)
  :group 'lsp-yaml
  :package-version '(lsp-mode . "10.0.0"))

(defun lsp-yaml-download-schema-store-db (&optional force-downloading)
  "Download remote schema store at `lsp-yaml-schema-store-uri' into local cache.
Set FORCE-DOWNLOADING to non-nil to force re-download the database."
  (interactive "P")
  (when (or force-downloading (not (file-exists-p lsp-yaml-schema-store-local-db)))
    (unless (file-directory-p (file-name-directory lsp-yaml-schema-store-local-db))
      (mkdir (file-name-directory lsp-yaml-schema-store-local-db) t))
    (url-copy-file lsp-yaml-schema-store-uri lsp-yaml-schema-store-local-db force-downloading)))

(defun lsp-yaml--get-supported-schemas ()
  "Get out the list of supported schemas."
  (when (and lsp-yaml-schema-store-enable
             (not lsp-yaml--schema-store-schemas-alist))
    (lsp-yaml-download-schema-store-db)
    (setq lsp-yaml--schema-store-schemas-alist
          (alist-get 'schemas (json-read-file lsp-yaml-schema-store-local-db))))
  (seq-concatenate 'list lsp-yaml-schema-extensions lsp-yaml--schema-store-schemas-alist))

(defun lsp-yaml-set-buffer-schema (uri-string)
  "Set yaml schema for the current buffer to URI-STRING."
  (interactive "MURI: ")
  (let* ((uri (intern uri-string))
         (workspace-path (file-relative-name
                          (lsp--uri-to-path (lsp--buffer-uri))
                          (lsp-workspace-root (lsp--buffer-uri))))
         (glob (concat "/" workspace-path))
         (current-config (assoc uri lsp-yaml-schemas))
         (current-patterns (and current-config (cdr current-config))))
    (if current-config
        (or (member glob (append current-patterns nil))
            (setq lsp-yaml-schemas
                  (cl-acons uri
                            (vconcat (vector glob) current-patterns)
                            (assq-delete-all uri
                                             (mapcar (lambda (x) (lsp-yaml--remove-glob x glob))
                                                     lsp-yaml-schemas)))))
      (setq lsp-yaml-schemas
            (cl-acons uri (vector glob) (mapcar (lambda (x) (lsp-yaml--remove-glob x glob))
                                                lsp-yaml-schemas))))
    (lsp--set-configuration (lsp-configuration-section "yaml"))))

(defun lsp-yaml-select-buffer-schema ()
  "Select schema for the current buffer based on the list of supported schemas."
  (interactive)
  (let* ((schema (lsp--completing-read "Select buffer schema: "
                                       (lsp-yaml--get-supported-schemas)
                                       (lambda (schema)
                                         (format "%s: %s" (alist-get 'name schema)(alist-get 'description schema)))
                                       nil t))
         (uri (alist-get 'url schema)))
    (lsp-yaml-set-buffer-schema uri)))

(defun lsp-yaml--remove-glob (mapping glob)
  (let ((patterns (cdr mapping)))
    (cons (car mapping)
          (vconcat (-filter (lambda (p) (not (equal p glob)))
                            (append patterns nil)) nil))))

(lsp-consistency-check lsp-yaml)

(provide 'lsp-yaml)
;;; lsp-yaml.el ends here
