#+title:  Programming Ansible
#+author: Howard X. Abrams
#+date:   2024-06-07
#+tags: emacs

Configuring Ansible and YAML

#+begin_src emacs-lisp :exports none
  ;;; ha-programming-ansible.el --- Configuring Ansible and YAML -*- lexical-binding: t; -*-
  ;;
  ;; © 2024 Howard X. Abrams
  ;;   Licensed under a Creative Commons Attribution 4.0 International License.
  ;;   See http://creativecommons.org/licenses/by/4.0/
  ;;
  ;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
  ;; Maintainer: Howard X. Abrams <howard.abrams@gmail.com>
  ;; Created: June  7, 2024
  ;;
  ;; While obvious, GNU Emacs does not include this file
  ;;
  ;; *NB:* Do not edit this file. Instead, edit the original literate file at:
  ;;            ~/src/hamacs/ha-programming-ansible.org
  ;;       And tangle the file to recreate this one.
  ;;
  ;;; Code:
  #+end_src

My day job now involves a lot of Ansible, and I’ve been struggling to get the right balance here.

Much of the conflict stems from whether to use [[file:ha-programming.org::*Tree Sitter][Tree Sitter]] for the YAML mode or not.
* Visual Indentation
Moving by lines is our default navigation mode, but for Yaml and Python, that doesn’t have s-expressions, we need something to move around by blocks. The [[https://codeberg.org/ideasman42/emacs-spatial-navigate][spatial navigation]] project attempts to address this. The problem is how to bind the functions.

The obvious keybindings are ~M-h/j/k/l~ … but that is used … well, somewhat. In org files, this is a way to move the outline of subtrees around. Useful, and =spatial-navigate= wouldn’t be helpful in org files anyway.

#+begin_src emacs-lisp :tangle no
  (use-package spatial-navigate
    :straight (:repo "https://codeberg.org/ideasman42/emacs-spatial-navigate")
    :config
    (pretty-hydra-define spatial-navigate (:color amaranth :quit-key "q")
      ("Box"
       (("k" spatial-navigate-backward-vertical-box "up")
        ("j" spatial-navigate-forward-vertical-box "Down")
        ("h" spatial-navigate-backward-horizontal-box "Down")
        ("l" spatial-navigate-forward-horizontal-box "Down"))
       "Bar"
       (("K" spatial-navigate-backward-vertical-bar "up")
        ("J" spatial-navigate-forward-vertical-bar "Down")
        ("H" spatial-navigate-backward-horizontal-bar "Down")
        ("L" spatial-navigate-forward-horizontal-bar "Down")))))
#+end_src

And we can attach this menu to the “g” section:
#+begin_src emacs-lisp :tangle no
  (use-package spatial-navigate
    :general
    (:states '(normal visual motion operator)
         "g t" '("spatial nav" . spatial-navigate/body)))
#+end_src

The [[https://github.com/antonj/Highlight-Indentation-for-Emacs][Highlight-Indentation]] project highlights each of the indent levels, which could be helpful for these modes:

#+begin_src emacs-lisp
  (use-package highlight-indentation
    :straight (:host github :repo "antonj/Highlight-Indentation-for-Emacs")
    :hook ((yaml-mode . highlight-indentation-mode)
           (yaml-ts-mode . highlight-indentation-mode)
           (python-mode . highlight-indentation-mode)))
#+end_src

This project has another display feature, which just lines up the current level. So I’ve created a toggle for this:

#+begin_src emacs-lisp
  (use-package highlight-indentation
    :config
    (setq highlight-indentation-blank-lines t)
    (set-face-background 'highlight-indentation-face "#332c26")
    (set-face-background 'highlight-indentation-current-column-face "#66615c")

    (defun ha-toggle-highlight-indentation ()
      "Toggles through the indentation modes."
      (interactive)
      (cond
       (highlight-indentation-mode
        (progn
          (highlight-indentation-mode -1)
          (highlight-indentation-current-column-mode 1)))
       (highlight-indentation-current-column-mode
        (progn
          (highlight-indentation-mode -1)
          (highlight-indentation-current-column-mode -1)))
       (t
        (progn
          (highlight-indentation-mode 1)
          (highlight-indentation-current-column-mode -1)))))

    (ha-leader "t i" '("show indents" . ha-toggle-highlight-indentation)))
#+end_src

* YAML
Doing a lot of [[https://github.com/yoshiki/yaml-mode][YAML work]], but  the =yaml-mode= project needs a new maintainer, so I might as well switch over to the T version.
, so I’ve switch to [[https://github.com/zkry/yaml-pro][yaml-pro]] that is now based on Tree Sitter. Let’s make sure the Tree-Sitter version works:

#+begin_src emacs-lisp
  (when (treesit-available-p)
    (use-package yaml-ts-mode
      :straight (:type built-in)
      :after mixed-pitch
      :mode ((rx ".yamllint")
             (rx ".y" (optional "a") "ml" string-end))
      :hook (yaml-ts-mode . (lambda () (mixed-pitch-mode -1)))
      :mode-hydra
      ((:foreign-keys run)
       ("Simple"
        (("l" ha-yaml-next-section "Next section")
         ("h" ha-yaml-prev-section "Previous"))))))
#+end_src

Allow this mode in Org blocks:
#+begin_src emacs-lisp :results silent
  (add-to-list 'org-babel-load-languages '(yaml-ts . t))
#+end_src

And we hook
#+begin_src emacs-lisp
  (use-package yaml-pro
    :straight (:host github :repo "zkry/yaml-pro")
    :after yaml-ts-mode
    :hook ((yaml-ts-mode . yaml-pro-ts-mode)
           (yaml-mode . yaml-pro-mode)))
#+end_src

Since I can never remember too many keybindings for particular nodes, we create a Hydra just for it.

#+begin_src emacs-lisp
  (use-package major-mode-hydra
    :after yaml-pro
    :config
    (major-mode-hydra-define yaml-ts-mode (:foreign-keys run)
      ("Navigation"
       (("u" yaml-pro-ts-up-level "Up level" :color pink) ; C-c C-u
        ("J" yaml-pro-ts-next-subtree "Next subtree" :color pink) ; C-c C-n
        ("K" yaml-pro-ts-prev-subtree "Previous" :color pink)) ; C-c C-p
       "Editing"
       (("m" yaml-pro-ts-mark-subtree "Mark subtree")  ; C-c C-@
        ("x" yaml-pro-ts-kill-subtree "Kill subtree")  ; C-c C-x C-w
        ("p" yaml-pro-ts-paste-subtree "Paste subtree")) ; C-c C-x C-y
       "Insert"
       (("e" yaml-pro-edit-ts-scalar "Edit item") ; C-c '
        ("o" yaml-pro-ts-meta-return "New list item"))
       "Refactor"
       (("r" yaml-pro-ts-move-subtree-up "Raise subtree")
        ("t" yaml-pro-ts-move-subtree-down "Lower subtree")
        ("," combobulate-hydra/body ">>>"))
       "Documentation"
       (("d" hydra-devdocs/body "Devdocs")))))
#+end_src

Note that these packages need the following to run properly:
#+begin_src sh
  pip install yamllint
#+end_src
* Jinja2
A lot of projects (like Ansible and Zuul) uses [[https://jinja.palletsprojects.com][Jinja2]] with YAML, so we first install the [[https://github.com/paradoxxxzero/jinja2-mode][jinja2-mode]]:
#+begin_src emacs-lisp
  (use-package jinja2-mode
    :mode (rx ".j2" string-end))
#+end_src

Jinja is a /template/ system that integrates /inside/ formats like JSON, HTML or YAML.
The [[https://polymode.github.io/][polymode]] project /glues/ modes like [[https://github.com/paradoxxxzero/jinja2-mode][jinja2-mode]] to [[https://github.com/yoshiki/yaml-mode][yaml-mode]].

I adapted this code from the [[https://github.com/emacsmirror/poly-ansible][poly-ansible]] project:
#+begin_src emacs-lisp
  (use-package polymode
    :config
    (define-hostmode poly-yaml-hostmode :mode 'yaml-ts-mode)

    (defcustom pm-inner/jinja2
      (pm-inner-chunkmode :mode #'jinja2-mode
                          :head-matcher (rx "{"
                                            (or "%" "{" "#")
                                            (optional (or "+" "-")))
                          :tail-matcher (rx (optional (or "+" "-"))
                                            (or "%" "}" "#")
                                            "}")
                          :head-mode 'body
                          :tail-mode 'body
                          :head-adjust-face t)
      "Jinja2 chunk."
      :group 'innermodes
      :type 'object)

    (define-polymode poly-yaml-jinja2-mode
      :hostmode 'poly-yaml-hostmode
      :innermodes '(pm-inner/jinja2))

    (major-mode-hydra-define+ yaml-ts-mode nil
      ("Extensions" (("j" poly-yaml-jinja2-mode "Jinja2")))))
#+end_src

We need to make sure the =mixed-pitch-mode= doesn’t screw things up.

#+begin_src emacs-lisp
  (add-hook 'poly-yaml-jinja2-mode-hook (lambda () (mixed-pitch-mode -1)))
#+end_src

We /can/ hook this up to Org, via:

#+begin_src emacs-lisp
  (add-to-list 'org-babel-load-languages '(poly-yaml-jinja2 . t))
#+end_src

Now we can use either =yaml-ts= or =poly-yaml-jinja2= (which perhaps we should make an alias?):

#+begin_src poly-yaml-jinja2 :tangle no
  ---
  # Let's see how this works
  - name: Busta move
    debug:
    msg: >-
      This {{ adjective }} {{ noun }} {{ verb }} the ball."
      {% for x in does %}
      What is this about?
      {% endfor %}
    vars:
      adjective: small
      noun: squirrel
      verb: ate
#+end_src
* Ansible
Do I consider all YAML files an Ansible file needing [[https://github.com/k1LoW/emacs-ansible][ansible-mode]]? Maybe we just have a toggle for when we want the Ansible feature.
#+begin_src emacs-lisp
  (use-package ansible
    :straight (:host github :repo "k1LoW/emacs-ansible")
    ;; :mode ((rx (or "playbooks" "roles") (one-or-more any) ".y" (optional "a") "ml") . ansible-mode)
    :config
    (setq ansible-vault-password-file "~/.ansible-vault-passfile")
    (major-mode-hydra-define+ yaml-ts-mode nil
       ("Extensions" (("a" ansible "Ansible"))))
    (ha-leader "t y" 'ansible))
#+end_src

The [[help:ansible-vault-password-file][ansible-vault-password-file]] variable needs to change /per project/, so let’s use the =.dir-locals.el= file, for instance:
#+begin_src emacs-lisp :tangle no
  ((nil . ((ansible-vault-password-file . "playbooks/.vault-password"))))
#+end_src

The YAML files get access Ansible’s documentation using the [[https://github.com/emacsorphanage/ansible-doc][ansible-doc]] project (that accesses the [[https://docs.ansible.com/ansible/latest/cli/ansible-doc.html][ansible-doc interface]]):

#+begin_src emacs-lisp
  (use-package ansible-doc
    :after yaml-ts-mode
    :hook (yaml-ts-mode . ansible-doc-mode)
    :config
    ;; (add-to-list 'exec-path (expand-file-name "~/.local/share/mise/installs/python/3.10/bin/ansible-doc"))
    (major-mode-hydra-define+ yaml-ts-mode nil
       ("Documentation"
        (("D" ansible-doc "Ansible")))))
#+end_src

Can we integrate Ansible with LSP using [[https://github.com/ansible/ansible-language-server][ansible-language-server]] project (see [[https://emacs-lsp.github.io/lsp-mode/page/lsp-ansible/][this documentation]])?

Using =npm= to install the program:
#+begin_src sh
  npm install -g @ansible/ansible-language-server
#+end_src
But … will I get some use out of this? I’ll come back to it later.


* Technical Artifacts                                :noexport:

Let's provide a name so that the file can be required:

#+begin_src emacs-lisp :exports none
  (provide 'ha-programming-ansible)
  ;;; ha-programming-ansible.el ends here
  #+end_src


#+DESCRIPTION: Configuring Ansible and YAML

#+PROPERTY:    header-args:sh :tangle no
#+PROPERTY:    header-args:emacs-lisp  :tangle yes
#+PROPERTY:    header-args    :results none :eval no-export :comments no mkdirp yes

#+OPTIONS:     num:nil toc:t todo:nil tasks:nil tags:nil date:nil
#+OPTIONS:     skip:nil author:nil email:nil creator:nil timestamp:nil
#+INFOJS_OPT:  view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js

# Local Variables:
# eval: (add-hook 'after-save-hook #'org-babel-tangle t t)
# End:
