Adjust indentation at work.
+requires
+
+#+begin_src sh :eval never
+ port install tree-sitter-yaml
+#+end_src
+
#+BEGIN_SRC emacs-lisp
;; load yaml-mode
(require 'yaml-mode)
(add-hook 'yaml-mode-hook
(lambda()
(auto-fill-mode -1)
+ (yaml-pro-ts-mode 1)
+ (eldoc-mode 1)
;; (stripes-mode 1)
(setq bba-keep-whitespaces 1)))
--- /dev/null
+;;; yaml-pro-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*-
+;; Generated by the `loaddefs-generate' function.
+
+;; This file is part of GNU Emacs.
+
+;;; Code:
+
+(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path)))
+
+
+\f
+;;; Generated autoloads from yaml-pro.el
+
+(autoload 'yaml-pro-ts-mode "yaml-pro" "\
+Minor mode to enable yaml-pro treesitter keymap.
+
+\\{yaml-pro-mode-map}
+
+This is a minor mode. If called interactively, toggle the `Yaml-Pro-Ts
+mode' mode. If the prefix argument is positive, enable the mode, and if
+it is zero or negative, disable the mode.
+
+If called from Lisp, toggle the mode if ARG is `toggle'. Enable the
+mode if ARG is nil, omitted, or is a positive number. Disable the mode
+if ARG is a negative number.
+
+To check whether the minor mode is enabled in the current buffer,
+evaluate the variable `yaml-pro-ts-mode'.
+
+The mode's hook is called both when the mode is enabled and when it is
+disabled.
+
+(fn &optional ARG)" t)
+(autoload 'yaml-pro-mode "yaml-pro" "\
+Binds additional functions to aid in editing YAML files.
+
+\\{yaml-pro-mode-map}
+
+This is a minor mode. If called interactively, toggle the `Yaml-Pro
+mode' mode. If the prefix argument is positive, enable the mode, and if
+it is zero or negative, disable the mode.
+
+If called from Lisp, toggle the mode if ARG is `toggle'. Enable the
+mode if ARG is nil, omitted, or is a positive number. Disable the mode
+if ARG is a negative number.
+
+To check whether the minor mode is enabled in the current buffer,
+evaluate the variable `yaml-pro-mode'.
+
+The mode's hook is called both when the mode is enabled and when it is
+disabled.
+
+(fn &optional ARG)" t)
+(register-definition-prefixes "yaml-pro" '("yaml-pro-"))
+
+\f
+;;; Generated autoloads from yaml-pro-edit.el
+
+(autoload 'yaml-pro-edit-scalar "yaml-pro-edit" "\
+Edit the scalar value at the point in a separate buffer.
+If prefix argument P is provided, prompt user for initialization command.
+
+(fn P)" t)
+(register-definition-prefixes "yaml-pro-edit" '("yaml-pro-edit-"))
+
+\f
+;;; Generated autoloads from yaml-pro-format.el
+
+(register-definition-prefixes "yaml-pro-format" '("yaml-pro-format"))
+\f
+;;; End of scraped data
+
+(provide 'yaml-pro-autoloads)
+
+;; Local Variables:
+;; version-control: never
+;; no-byte-compile: t
+;; no-update-autoloads: t
+;; no-native-compile: t
+;; coding: utf-8-emacs-unix
+;; End:
+
+;;; yaml-pro-autoloads.el ends here
--- /dev/null
+;;; yaml-pro-edit.el --- Functions for editing functionality -*- lexical-binding: t -*-
+
+;; Author: Zachary Romero
+;; Maintainer: Zachary Romero
+;; Homepage: https://github.com/zkry/yaml-pro
+;; Keywords: tools
+
+;; This file is not part of GNU Emacs
+
+;; 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:
+
+;; M-x yaml-pro-edit-ts-scalar
+
+;;; Code:
+
+(require 'treesit nil t)
+(require 'yaml)
+
+(defconst yaml-pro-edit-mode-map
+ (let ((map (make-sparse-keymap)))
+ (prog1 map
+ ;;(suppress-keymap map)
+ (define-key map (kbd "C-c C-k") #'yaml-pro-edit-quit)
+ (define-key map (kbd "C-c C-c") #'yaml-pro-edit-complete)
+ (define-key map (kbd "C-c C-s") #'yaml-pro-edit-change-output)))
+ "Keymap of yaml-edit-mode.")
+
+(defconst yaml-pro-edit-buffer-name "*yaml-pro-edit*")
+(defvar-local yaml-pro-edit-ts-node-position nil)
+(put 'yaml-pro-edit-ts-node-position 'permanent-local t)
+(defvar-local yaml-pro-edit-scalar nil
+ "The current scalar being edited on.
+Used to fetch location properties on completion.")
+(put 'yaml-pro-edit-scalar 'permanent-local t)
+(defvar-local yaml-pro-edit-scalar-overlay nil
+ "Overlay to go on top of the text currently being edited.")
+(put 'yaml-pro-edit-scalar-overlay 'permanent-local t)
+(defvar-local yaml-pro-edit-parent-buffer nil
+ "Reference to the buffer where the edited YAML originated.")
+(put 'yaml-pro-edit-parent-buffer 'permanent-local t)
+(defvar-local yaml-pro-edit-output-type nil
+ "When completing an edit buffer, output scalar according to this.")
+(put 'yaml-pro-edit-output-type 'permanent-local t)
+(defvar-local yaml-pro-edit-initialization-cache nil
+ "Hashmap of YAML path to initialization function.")
+(put 'yaml-pro-edit-initialization-cache 'permanent-local t)
+
+
+(define-minor-mode yaml-pro-edit-mode
+ "Minor-mode for editing a YAML scalar in a separate buffer."
+ :lighter " YAML-pro"
+ :keymap yaml-pro-edit-mode-map
+ (add-hook 'change-major-mode-hook
+ (lambda ()
+ (let ((buf (current-buffer)))
+ ;; Setting the header-line-format in the
+ ;; change-major-mode-hook doesn't work. Probably some
+ ;; major mode thing is done after the hook call. This
+ ;; timer hack makes sure the header-line-format is set
+ ;; properly.
+ (run-with-timer
+ 0.01 nil
+ (lambda ()
+ (with-current-buffer buf
+ (setq header-line-format (yaml-pro-edit--header-line)))))))
+ nil t))
+(put 'yaml-pro-edit-mode 'permanent-local t)
+
+(defun yaml-pro-edit-cleanup-parent ()
+ "Remove overlay and properties of edited text."
+ (with-current-buffer (or yaml-pro-edit-parent-buffer (current-buffer))
+ (when yaml-pro-edit-scalar-overlay
+ (delete-overlay yaml-pro-edit-scalar-overlay)
+ (setq yaml-pro-edit-scalar-overlay nil))))
+
+(defun yaml-pro-edit--infer-scalar-indent (scalar-block-string)
+ "Infer the scalar indentation SCALAR-BLOCK-STRING."
+ (with-temp-buffer
+ (insert scalar-block-string)
+ (goto-char (point-min))
+ (save-excursion
+ (let ((min-indent 10000))
+ (while (not (eobp))
+ (when (not (looking-at " *\n"))
+ (let ((ct 0))
+ (while (looking-at-p " ")
+ (forward-char 1)
+ (cl-incf ct))
+ (when (< ct min-indent)
+ (setq min-indent ct))))
+ (forward-line 1))
+ min-indent))))
+
+(defun yaml-pro-edit--infer-indent (&optional pos)
+ "Infer the YAML indentation of POS or the current point."
+ (save-excursion
+ (let ((pos (or pos (point))))
+ (goto-char pos)
+ (when (and (eobp) ;; fix edge where end of scalar at eop has end after newline
+ (eql (char-before (point)) ?\n))
+ (forward-line -1))
+ (forward-line 0)
+ (skip-chars-forward "- \n")
+ (current-column))))
+
+(defconst yaml-pro-edit-output-types
+ '(("|" . literal)
+ ("|-" . literal-strip)
+ ("|+" . literal-keep)
+ (">" . folded)
+ (">-" . folded-strip)
+ (">+" . folded-keep)
+ ("'" . single)
+ ("\"" . double))
+ "Alist of selection item to type key.")
+
+(defun yaml-pro-edit--block-style-annotation (v)
+ "Return the completion annotation of the selection string V."
+ (let ((annotations
+ '(("|" . "keep newlines, single newline at end")
+ ("|-" . "keep newlines, strip newlines at end")
+ ("|+" . "keep newlines, keep newlines at end")
+ (">" . "fold newlines to space, single newline at end")
+ (">-" . "fold newlines to space, strip newlines at end")
+ (">+" . "fold newlines to space, keep newlines at end")
+ ("'" . "nothing escaped")
+ ("\"" . "newlines escaped"))))
+ (concat " " (cdr (assoc v annotations)))))
+
+(defun yaml-pro-edit--block-output (type)
+ "Return the block prefix of TYPE if one exists."
+ (when (not (memq type '(single double)))
+ (car (rassoc type yaml-pro-edit-output-types))))
+
+(defun yaml-pro-edit-apply-indentation (content indent &optional type)
+ "Apply an indentation level of INDENT to the string CONTENT and return."
+ (with-temp-buffer
+ (insert content)
+ (goto-char (point-min))
+ (let ((indent-str (make-string indent ?\s)))
+ (when (eql type 'single)
+ (forward-line 1))
+ (while (not (eobp))
+ (when (not (looking-at-p "^ *\n"))
+ (insert indent-str))
+ (forward-line 1)))
+ (buffer-string)))
+
+(defun yaml-pro-edit--header-line ()
+ "Return the string to display in the header line."
+ (let* ((base-text (substitute-command-keys "Edit, then exit with \
+`\\[yaml-pro-edit-complete]' or abort with `\\[yaml-pro-edit-quit]'.")))
+ (if yaml-pro-edit-output-type
+ (let* ((type-str
+ (or (yaml-pro-edit--block-output yaml-pro-edit-output-type)
+ (and (eql yaml-pro-edit-output-type 'double) "\"\"")
+ (and (eql yaml-pro-edit-output-type 'single) "''")))
+ (type-str-prp (propertize type-str 'face 'font-lock-string-face)))
+ (concat base-text
+ "Outputting "
+ type-str-prp
+ (substitute-command-keys
+ ", `\\[yaml-pro-edit-change-output]' to change out type")))
+ (concat base-text
+ (substitute-command-keys
+ "Change output with `\\[yaml-pro-edit-change-output]'")))))
+
+(defun yaml-pro-edit-initialize-buffer
+ (parent-buffer buffer initial-text type initialize path)
+ "Initialize the YAML edit buffer.
+
+PARENT-BUFFER is the buffer from which the YAML was copied.
+BUFFER is the editing buffer. The buffer is initialized with the
+text INITIAL-TEXT.
+
+TYPE indicates the type of scalar that the block was copied
+from (ex literal or folded).
+
+INITIALIZE, if non-nil, will run before the buffer is
+initialized. The INITIALIZE function will be cached under PATH,
+resulting in the function being ran upon subsequent edits."
+ (when (not yaml-pro-edit-initialization-cache)
+ (setq-local yaml-pro-edit-initialization-cache
+ (make-hash-table :test 'equal)))
+ (let* ((init-cache yaml-pro-edit-initialization-cache)
+ (init-fn (or initialize (and path (gethash path init-cache)))))
+ (with-current-buffer buffer
+ (when (functionp init-fn)
+ (condition-case e
+ (progn
+ (funcall init-fn)
+ (when (and initialize path)
+ (puthash path initialize init-cache)))
+ (error (message "Initialization failed with: %S"
+ (error-message-string e)))))
+ (unless yaml-pro-edit-mode
+ (yaml-pro-edit-mode))
+ (erase-buffer)
+ (setq-local require-final-newline nil)
+ (setq-local yaml-pro-edit-parent-buffer parent-buffer)
+ (setq-local yaml-pro-edit-output-type nil)
+ (when type (setq-local yaml-pro-edit-output-type type))
+ (setq header-line-format (yaml-pro-edit--header-line))
+ (insert initial-text))))
+
+(defun yaml-pro-edit--extract-scalar-text (scalar-block-string yaml-indentation)
+ "Strip SCALAR-BLOCK-STRING of indentation with a basis of YAML-INDENTATION."
+ ;; TODO: consider consolidating this logic with that of yaml.el
+ (save-match-data
+ (let ((indent-ct-num
+ (progn (string-match "\\`[^\n]*\\([0-9]+\\) *\n" scalar-block-string)
+ (let ((num-str (match-string 1 scalar-block-string)))
+ (and num-str (+ (string-to-number num-str)
+ yaml-indentation))))))
+ (setq scalar-block-string (string-trim-left scalar-block-string ".*\n"))
+ (with-temp-buffer
+ (insert scalar-block-string)
+ (goto-char (point-min))
+ (let* ((indentation
+ (or indent-ct-num
+ (yaml-pro-edit--infer-scalar-indent scalar-block-string)))
+ (indentation-regexp
+ (regexp-quote (make-string indentation ?\s))))
+ (while (not (eobp))
+ (when (looking-at-p indentation-regexp)
+ (delete-char indentation))
+ (forward-line 1)))
+ (buffer-string)))))
+
+;; Interactive Functions
+
+(defun yaml-pro-edit-change-output ()
+ "Change the selected output type after completing the YAML."
+ (interactive)
+ (unless yaml-pro-edit-mode
+ (user-error "not in yaml-pro edit buffer"))
+ (let* ((completion-extra-properties
+ '(:annotation-function yaml-pro-edit--block-style-annotation))
+ (output-type (completing-read "Output: " yaml-pro-edit-output-types))
+ (key (cdr (assoc output-type yaml-pro-edit-output-types 'equal))))
+ (setq yaml-pro-edit-output-type key)
+ (setq header-line-format (yaml-pro-edit--header-line))))
+
+(defun yaml-pro-edit-quit ()
+ "Quit the edit buffer."
+ (interactive)
+ (unless yaml-pro-edit-mode
+ (user-error "not in yaml-pro edit buffer"))
+ (yaml-pro-edit-cleanup-parent)
+ (let ((b (current-buffer)))
+ (quit-window)
+ (kill-buffer b)))
+
+(defun yaml-pro-edit-complete ()
+ "Deleting edit buffer and move its contents to the original YAML buffer."
+ (interactive)
+ (unless yaml-pro-edit-mode
+ (user-error "not in yaml-pro edit buffer"))
+ (unless yaml-pro-edit-parent-buffer
+ (error "Buffer not connected with yaml buffer"))
+ ;; first ensure that buffer ends with at least one newline
+ (let* ((edit-buf (current-buffer))
+ (edit-str (buffer-substring-no-properties (point-min) (point-max)))
+ (type yaml-pro-edit-output-type))
+ (when (not (memq type '(single double)))
+ (save-excursion
+ (goto-char (point-max))
+ (unless (looking-back "\n" (- (point) 2))
+ (insert "\n")
+ (setq edit-str (buffer-substring-no-properties (point-min) (point-max))))))
+ (save-excursion
+ (with-current-buffer yaml-pro-edit-parent-buffer
+ (let* ((pos (or (and yaml-pro-ts-mode yaml-pro-edit-ts-node-position)
+ (get-text-property 0 'yaml-position yaml-pro-edit-scalar)))
+ (start (car pos))
+ (end (cdr pos))
+ (indent (yaml-pro-edit--infer-indent start))
+ (scalar-indent (yaml-pro-edit--infer-scalar-indent edit-str))
+ (indented-edit-str
+ (yaml-pro-edit-apply-indentation edit-str (+ indent 2) type))
+ (block-header (or (yaml-pro-edit--block-output type)
+ (and (not type)
+ (> (length (split-string edit-str "\n")) 1)
+ ">-")))
+ (extract-string (buffer-substring-no-properties start end))
+ (end-newline-p (= (aref extract-string
+ (1- (length extract-string)))
+ ?\n)))
+ (yaml-pro-edit-cleanup-parent)
+ (delete-region start end)
+ (goto-char start)
+ (cond (block-header
+ (if (> scalar-indent 0)
+ (insert block-header "2\n")
+ (insert block-header "\n"))
+ (insert indented-edit-str))
+ ((or (eql type 'double)
+ (and (not type)
+ (and (= (length (split-string edit-str "\n")) 1)
+ (string-prefix-p " " edit-str))))
+ (insert "\"" (replace-regexp-in-string "\"" "\\\\\""
+ (replace-regexp-in-string "\n" "\\\\n" edit-str))
+ "\""))
+ ((eql type 'single)
+ (insert "'" (string-trim-right (replace-regexp-in-string
+ "'" "''"
+ indented-edit-str))
+ "'"))
+ (t
+ (insert edit-str)))
+ ;; following blocks are tricky
+ ;; if the text being extracted is a block, then it ends in a newline
+ ;; meaning the resulting code should return a newline
+ ;; if the text being extracted is quoted, we need to make sure that
+ ;; the block doesn't add too many lines than needed.
+ (when (and (not (memq type '(double single)))
+ (not end-newline-p))
+ (delete-char 1))
+ (when (and (memq type '(double single))
+ end-newline-p)
+ (insert "\n")))))
+ (quit-window)
+ (kill-buffer edit-buf)))
+
+(declare-function yaml-pro--value-at-point "yaml-pro")
+(declare-function yaml-pro--fast-value-at-point "yaml-pro")
+(declare-function yaml-pro--path-at-point "yaml-pro")
+(declare-function yaml-pro--use-fast-p "yaml-pro")
+
+(defun yaml-pro-edit-ts-scalar-node-at (point)
+ "Return the node at POINT corresponding with editable scalar."
+ (let* ((at-node (treesit-node-at point 'yaml)))
+ (cond
+ ((member (treesit-node-type at-node) '("block_scalar" "string_scalar"))
+ at-node)
+ ((member (treesit-node-type at-node) '("'" "\"" "escape_sequence"))
+ (treesit-node-parent at-node)))))
+
+(defun yaml-pro-edit-ts-strip-quotes (text)
+ "Strip the first and last quote character in TEXT."
+ (let ((is-single-quote (eql (aref text 0) ?'))
+ (base-string (substring text 1 (1- (length text)))))
+ (if is-single-quote
+ (replace-regexp-in-string " *\n+ *" " " base-string)
+ base-string)))
+
+(defun yaml-pro-edit-ts-scalar (p)
+ "Edit the scalar value at the point in a separate buffer.
+This command utilizes tree-sitter to detirmine syntax
+locations. If prefix argument P is provided, prompt user for
+initialization command."
+ (interactive "p")
+ (let* ((init-func)
+ (parent-buffer (current-buffer))
+ (at-node (yaml-pro-edit-ts-scalar-node-at (point)))
+ (start (treesit-node-start at-node))
+ (end (treesit-node-end at-node))
+ (node-text (treesit-node-text at-node)))
+ (when (= 4 p)
+ (setq init-func (read-command "Initialization command: ")))
+ (unless at-node
+ (error "No scalar found at point"))
+ (setq yaml-pro-edit-ts-node-position (cons start end))
+ (save-excursion
+ (goto-char (treesit-node-start at-node))
+ (let* ((yaml-indentation (yaml-pro-edit--infer-indent end))
+ (folded-block-p (looking-at-p ">"))
+ (literal-block-p (looking-at-p "|"))
+ (strip-p (looking-at-p ".-[0-9]+\n"))
+ (keep-p (looking-at-p ".+[0-9]+\n"))
+ (double-quote-string-p (equal "double_quote_scalar" (treesit-node-type at-node)))
+ (single-quote-string-p (equal "single_quote_scalar" (treesit-node-type at-node)))
+ (type (cond ((and folded-block-p strip-p) 'folded-strip)
+ ((and literal-block-p strip-p) 'literal-strip)
+ ((and folded-block-p keep-p) 'folded-keep)
+ ((and literal-block-p keep-p) 'literal-keep)
+ (folded-block-p 'folded)
+ (literal-block-p 'literal)
+ (single-quote-string-p 'single)
+ (double-quote-string-p 'double))))
+ (when yaml-pro-edit-scalar-overlay
+ (delete-overlay yaml-pro-edit-scalar-overlay))
+ (let ((ov (make-overlay start end))
+ (read-only
+ (list
+ (lambda (&rest _) (user-error "Can't modify a scalar being edited in a dedicated buffer")))))
+ (overlay-put ov 'modification-hooks read-only)
+ (overlay-put ov 'insert-in-front-hooks read-only)
+ (overlay-put ov 'insert-behind-hooks read-only)
+ (overlay-put ov 'face 'secondary-selection)
+ (setq yaml-pro-edit-scalar-overlay ov))
+ (let ((b (get-buffer-create yaml-pro-edit-buffer-name)))
+ (if (or folded-block-p literal-block-p)
+ (yaml-pro-edit-initialize-buffer
+ parent-buffer b (yaml-pro-edit--extract-scalar-text node-text yaml-indentation) type init-func nil)
+ (yaml-pro-edit-initialize-buffer
+ parent-buffer b (yaml-parse-string node-text) type init-func nil))
+ (let ((window (display-buffer b)))
+ (when window
+ (select-window window))))))))
+
+;;;###autoload
+(defun yaml-pro-edit-scalar (p)
+ "Edit the scalar value at the point in a separate buffer.
+If prefix argument P is provided, prompt user for initialization command."
+ (interactive "p")
+ (let* ((init-func)
+ (fast-p (yaml-pro--use-fast-p))
+ (at-scalar (if fast-p (yaml-pro--fast-value-at-point)
+ (yaml-pro--value-at-point)))
+ (path (and (not fast-p) (yaml-pro--path-at-point)))
+ (parent-buffer (current-buffer)))
+ (unless at-scalar
+ (user-error "No value found at point"))
+ (when (= 4 p)
+ (setq init-func (read-command "Initialization command: ")))
+ (setq yaml-pro-edit-scalar at-scalar)
+ (let* ((bounds (or (get-text-property 0 'yaml-position at-scalar)
+ (get-text-property 0 'yaml-position (string-trim at-scalar "[ \n]*"))))
+ (start (car bounds))
+ (end (cdr bounds))
+ (yaml-indentation (yaml-pro-edit--infer-indent start))
+ (scalar-text (buffer-substring start end))
+ (raw-scalar (yaml-pro-edit--extract-scalar-text
+ scalar-text yaml-indentation))
+ (folded-block-p
+ (string-match-p "\\` *>\\(?:+\\|-\\)?[0-9]*\n" scalar-text))
+ (literal-block-p
+ (string-match-p "\\` *|\\(?:+\\|-\\)?[0-9]*\n" scalar-text))
+ (strip-p
+ (string-match-p "\\` *.-[0-9]*\n" scalar-text))
+ (keep-p
+ (string-match-p "\\` *.\\+[0-9]*\n" scalar-text))
+ (double-quote-string-p
+ (string-match-p "\\`\".*\"\\'" scalar-text))
+ (single-quote-string-p
+ (string-match-p "\\`'\\(.\\|\n\\)*'\\'" scalar-text))
+ (type (cond ((and folded-block-p strip-p) 'folded-strip)
+ ((and literal-block-p strip-p) 'literal-strip)
+ ((and folded-block-p keep-p) 'folded-keep)
+ ((and literal-block-p keep-p) 'literal-keep)
+ (folded-block-p 'folded)
+ (literal-block-p 'literal)
+ (single-quote-string-p 'single)
+ (double-quote-string-p 'double))))
+ ;; setup overlay and text properties
+ (when yaml-pro-edit-scalar-overlay
+ (delete-overlay yaml-pro-edit-scalar-overlay))
+ (let ((ov (make-overlay start end))
+ (read-only
+ (list
+ (lambda (&rest _)
+ (user-error "Can't modify an scalar being edited in a \
+dedicated buffer")))))
+ (overlay-put ov 'modification-hooks read-only)
+ (overlay-put ov 'insert-in-front-hooks read-only)
+ (overlay-put ov 'insert-behind-hooks read-only)
+ (overlay-put ov 'face 'secondary-selection)
+ (setq yaml-pro-edit-scalar-overlay ov))
+ (let ((b (get-buffer-create yaml-pro-edit-buffer-name)))
+ (if (or folded-block-p literal-block-p)
+ ;; we need to pass the text in buffer if type is block
+ (yaml-pro-edit-initialize-buffer
+ parent-buffer b raw-scalar type init-func path)
+ ;; otherwise use its scalar value (to not show quotes)
+ (yaml-pro-edit-initialize-buffer
+ parent-buffer b at-scalar type init-func path))
+ (let ((window (display-buffer b)))
+ (when window
+ (select-window window)))))))
+
+(provide 'yaml-pro-edit)
+
+;;; yaml-pro-edit.el ends here
--- /dev/null
+;;; yaml-pro-format.el --- Pretty formatting for YAML code -*- lexical-binding: t -*-
+
+;; Author: Zachary Romero
+;; Maintainer: Zachary Romero
+;; Package-Requires: ((emacs "28.1"))
+;; Homepage: https://github.com/zkry/yaml-pro
+;; Keywords: tools
+
+;; This file is not part of GNU Emacs
+
+;; 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:
+
+;; yaml-pro-format-ts is a command which will format the current
+;; buffer to make have uniform styling. The rules the formatter
+;; applies are defined in the `yaml-pro-format-features' variable. By
+;; changing its value you can enable and disable formatting rules.
+;;
+;; The pretty formatter was inspired by the prettier.js YAML formatter
+;; but does have a number of differences, some limited by the
+;; capabilities of Tree Sitter.
+
+;;; Code:
+
+(require 'treesit nil t)
+(require 'cl-lib)
+
+(defvar yaml-pro-indent)
+
+(defcustom yaml-pro-format-print-width 80
+ "Width until which to break flow sequences."
+ :group 'yaml-pro
+ :type 'integer)
+
+(defcustom yaml-pro-format-features
+ '(reduce-newlines document-separator-own-line oneline-flow block-formatting
+ reduce-spaces bm-fn-next-line clean-doc-end remove-spaces-before-comments
+ expand-long-flow single-to-double indent)
+ "Features to enable when formatting buffer.
+
+This value should be a list containing any of the following symbols:
+
+- `indent' When present, indent each line by `yaml-pro-indent'.
+
+- `reduce-newlines' When present, remove adjacent newlines so at
+ most one one remains. New lines in strings won't be removed.
+
+- `oneline-flow' When present, reduce flow mappings to one line.
+
+- `block-formatting' When present, format the spacing around
+ block components' colons and dashes.
+
+- `reduce-spaces' When present, attempt to reduce multiple
+ adjacent space characters down to one.
+
+- `bm-fn-next-line' When present, move the value of a key-value pair to the
+ next line, indented, if it's width is longer than
+ `yaml-pro-format-print-width'.
+
+- `expand-log-flow' When present, flatten flow elements that pass
+ column `yaml-pro-format-print-width'.
+
+- `single-to-double' When present, convert single quoted strings
+ to double quoted, when it wouldn't change its meaning.
+
+- `remove-spaces-before-comments' When present, remove spaces
+ before a comment until it is one space after the preceeding
+ content (e.g. \"a: b # space before here\").
+
+- `clean-doc-end' When present, remove unnecessary document-end
+ indicators (\"...\").
+
+- `document-separator-own-line' When present, makes sure the
+ document separator \"---\" isn't on a line with another element."
+ :group 'yaml-pro
+ :type '(set (const :tag "Remove adjacent blank lines, leaving at most one"
+ reduce-newlines)
+ (const :tag "Put document separators (---) are on a line of their own"
+ document-separator-own-line)
+ (const :tag "Try to reduce flow (JSON-like) elements to one line"
+ oneline-flow)
+ (const :tag "Format spacing around block elments' colon and dashes"
+ block-formatting)
+ (const :tag "Reduce spacing down to one when appropriate"
+ reduce-spaces)
+ (const :tag "Transform \"key: value\" to \"key:\\n value\" if column passes `yaml-pro-format-print-width'"
+ bm-fn-next-line)
+ (const :tag "Remove unnecessary document end indicator (...)"
+ clean-doc-end)
+ (const :tag "Leave only one space before comments"
+ remove-spaces-before-comments)
+ (const :tag "Flatten flow nodes if column passes `yaml-pro-format-print-width'"
+ expand-long-flow)
+ (const :tag "Convert single-quoted strings to double-quoted strings"
+ single-to-double)
+ (const :tag "Indent according to treesitter level information"
+ indent)))
+
+(defun yaml-pro-format-ts--bm-single-space ()
+ "Ensure proper spacing around the colon character in a block mapping."
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((block_mapping_pair
+ key: (flow_node) @key
+ ":" @colon
+ value: [(flow_node) (block_node)] @val))))
+ (del-ovs '()))
+ (while nodes
+ (save-excursion
+ (let* ((colon-node (alist-get 'colon nodes))
+ (key-node (alist-get 'key nodes))
+ (val-node (alist-get 'val nodes)))
+ (setq nodes (cdddr nodes))
+ (when (and (not (= (treesit-node-start colon-node)
+ (treesit-node-end key-node)))
+ ;; we should only be overwriting blank here
+ (string-match-p "\\`[ \n]*\\'"
+ (buffer-substring-no-properties
+ (treesit-node-end key-node)
+ (treesit-node-start colon-node)))
+ ;; alias names need a space after them
+ (not (equal (treesit-node-type
+ (treesit-node-at (treesit-node-end key-node) 'yaml))
+ "alias_name")))
+ (push (make-overlay (treesit-node-end key-node)
+ (treesit-node-start colon-node))
+ del-ovs))
+ (when
+ (and (or (string-suffix-p "_scalar" (treesit-node-type val-node))
+ (equal (treesit-node-type val-node) "flow_node"))
+ (not (= (1+ (treesit-node-end colon-node))
+ (treesit-node-start val-node)))
+ ;; the region should only replace blank
+ (string-match-p
+ "\\`[ \n]*\\'"
+ (buffer-substring-no-properties
+ (treesit-node-end colon-node)
+ (treesit-node-start val-node))))
+ (let ((ov (make-overlay (treesit-node-end colon-node)
+ (treesit-node-start val-node))))
+ (overlay-put ov 'yaml-pro-format-insert " ")
+ (push ov del-ovs))))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--oneline-flow ()
+ "Compress flow elements to one line."
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((document
+ (flow_node
+ [(flow_mapping) (flow_sequence)] @flow))
+ (block_mapping_pair
+ (flow_node
+ [(flow_mapping) (flow_sequence)] @flow))
+ (block_sequence_item
+ (flow_node
+ [(flow_mapping) (flow_sequence)] @flow)))))
+ (del-ovs '()))
+ (pcase-dolist (`(_ . ,node) nodes)
+ ;; don't compress flow with comments
+ (unless (treesit-query-capture node '((comment) @comment))
+ (save-match-data
+ (save-excursion
+ (goto-char (treesit-node-start node))
+ (while (search-forward "\n" (treesit-node-end node) t)
+ (unless (equal (treesit-node-type
+ (treesit-node-at (match-beginning 0) 'yaml))
+ "comment")
+ (let* ((ov (make-overlay (match-beginning 0) (match-end 0))))
+ (push ov del-ovs))))))))
+ ;; Following code ensures that compressed flow is pretty formatted
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((flow_mapping "," @comma)))))
+ (pcase-dolist (`(_ . ,comma-node) nodes)
+ (cond
+ ((save-excursion (goto-char (treesit-node-end comma-node))
+ (looking-at-p " *\n? *}"))
+ (let* ((ov (make-overlay (treesit-node-start comma-node)
+ (treesit-node-end comma-node))))
+ (overlay-put ov 'yaml-pro-format-insert " ")
+ (push ov del-ovs)))
+ ((save-excursion (goto-char (treesit-node-start comma-node))
+ (looking-back "[^\n] *" (- (point) 80)))
+ (let* ((ov (make-overlay (save-excursion
+ (goto-char (treesit-node-start comma-node))
+ (skip-chars-backward " ")
+ (point))
+ (treesit-node-start comma-node))))
+ (push ov del-ovs))))))
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((flow_mapping "}" @rbrace)))))
+ (pcase-dolist (`(_ . ,rbrace-node) nodes)
+ (when (save-excursion (goto-char (treesit-node-start rbrace-node))
+ (not (looking-back "[ \n{] ?" (- (point) 4))))
+ (let* ((ov (make-overlay (treesit-node-start rbrace-node)
+ (treesit-node-start rbrace-node))))
+ (overlay-put ov 'yaml-pro-format-insert " ")
+ (push ov del-ovs)))))
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((flow_mapping "{" @lbrace)))))
+ (pcase-dolist (`(_ . ,lbrace-node) nodes)
+ (when (save-excursion (goto-char (treesit-node-end lbrace-node))
+ (not (looking-at-p "[ \n}]")))
+ (let* ((ov (make-overlay (treesit-node-end lbrace-node)
+ (treesit-node-end lbrace-node))))
+ (overlay-put ov 'yaml-pro-format-insert " ")
+ (push ov del-ovs)))))
+ del-ovs))
+
+
+(defun yaml-pro-format-ts--no-comments-after-tags ()
+ "Move comments to a newline if they occur after a tag (e.g. !!set)."
+ (let* ((del-ovs '())
+ (nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((_ (tag) @tag :anchor (comment) @comment)))))
+ (while nodes
+ (let* ((tag-node (alist-get 'tag nodes))
+ (comment-node (alist-get 'comment nodes)))
+ (setq nodes (cddr nodes))
+ (when (= (line-number-at-pos (treesit-node-end tag-node))
+ (line-number-at-pos (treesit-node-start comment-node)))
+ (let* ((ov (make-overlay (treesit-node-end tag-node)
+ (treesit-node-start comment-node))))
+ (overlay-put ov 'yaml-pro-format-insert "\n")
+ (push ov del-ovs)))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--flow-seq-grouping-space ()
+ "Remove all whitespace before and after flow nodes' [ ] chars."
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((flow_mapping) @flow
+ (flow_sequence) @flow)))
+ (del-ovs '()))
+ (pcase-dolist (`(_ . ,node) nodes)
+ (let-alist (treesit-query-capture
+ node
+ '((flow_sequence "[" @open :anchor (_) @after-open-node
+ (_) @before-close-node :anchor "]" @close)))
+ (when (and .open .close .after-open-node .before-close-node)
+ (if (= (line-number-at-pos (treesit-node-end .open))
+ (line-number-at-pos (treesit-node-start .close)))
+ ;; If [ ] is on one line, remove spacing after [ and before ].
+ (let* ((ov-beg (make-overlay (treesit-node-end .open)
+ (treesit-node-start .after-open-node)))
+ (ov-end (make-overlay (treesit-node-end .before-close-node)
+ (treesit-node-start .close))))
+ (push ov-beg del-ovs)
+ (unless (equal (treesit-node-type .before-close-node) "comment")
+ (push ov-end del-ovs)))
+ ;; If [ ] is on multiple lines, ensure [ ] are on their own lines.
+ (let* ((after-open-indent
+ (yaml-pro-format-ts--node-indent .after-open-node))
+ (ov-beg (make-overlay (treesit-node-end .open)
+ (treesit-node-start .after-open-node)))
+ (ov-end (make-overlay (treesit-node-end .before-close-node)
+ (treesit-node-start .close))))
+ (overlay-put ov-beg 'yaml-pro-format-insert
+ (concat "\n" (make-string
+ (* after-open-indent yaml-pro-indent)
+ ?\s)))
+ (overlay-put ov-end 'yaml-pro-format-insert
+ (concat ",\n" (make-string
+ (* (- after-open-indent 1)
+ yaml-pro-indent)
+ ?\s)))
+ (push ov-beg del-ovs)
+ (unless (equal (treesit-node-type .before-close-node) "comment")
+ (push ov-end del-ovs)))))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--bm-fn-next-line ()
+ "If a flow node passes the 80th column, put it on the next line."
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((block_mapping_pair
+ ":" @colon :anchor value: (flow_node) @child)
+ @parent)))
+ (del-ovs '()))
+ (while nodes
+ (let-alist (seq-take nodes 3)
+ (setq nodes (cdddr nodes))
+ (when (and (= (line-number-at-pos (treesit-node-start .parent))
+ (line-number-at-pos (treesit-node-start .child)))
+ (> (save-excursion (goto-char (treesit-node-end .child))
+ (current-column))
+ 80))
+ (let* ((indent (yaml-pro-format-ts--node-indent .child))
+ (ov (make-overlay (treesit-node-end .colon)
+ (treesit-node-start .child))))
+ (overlay-put
+ ov 'yaml-pro-format-insert
+ (concat "\n" (make-string (* indent yaml-pro-indent) ?\s)))
+ (push ov del-ovs)))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--clean-doc-end ()
+ "Remove the document end symbol (...)."
+ (let* ((del-ovs '())
+ (capture (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '(("..." @doc-end)))))
+ (pcase-dolist (`(_ . ,node) capture)
+ (save-excursion
+ (goto-char (treesit-node-end node))
+ (skip-chars-forward "\n \t")
+ (let* ((next-node (treesit-node-at (point) 'yaml)))
+ (cond
+ ((member (treesit-node-type next-node) '("comment" "directive_name"))
+ ;; leave alone if next node is a comment
+ )
+ ((equal (treesit-node-type next-node) "---")
+ (let* ((ov (make-overlay (1- (treesit-node-start node))
+ (treesit-node-end node))))
+ (push ov del-ovs)))
+ (t
+ (let* ((ov (make-overlay (treesit-node-start node)
+ (treesit-node-end node))))
+ (overlay-put ov 'yaml-pro-format-insert "---")
+ (push ov del-ovs)))))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--remove-spaces-before-comments ()
+ "Remove spaces before comments.
+If there is nothing before the comment on a line, move it to col 0.
+If there is other text, ensure one space remains."
+ (let* ((del-ovs '())
+ (capture (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((comment) @comment))))
+ (pcase-dolist (`(_ . ,comment-node) capture)
+ (save-excursion
+ (save-match-data
+ (goto-char (treesit-node-start comment-node))
+ (cond
+ ((looking-back "\n *" (- (point) 80))
+ (let* ((ov (make-overlay (pos-bol) (treesit-node-start comment-node))))
+ (push ov del-ovs)))
+ ((looking-back "[^\n] *" (- (point) 80))
+ (let* ((ov (make-overlay
+ (save-excursion (goto-char (treesit-node-start comment-node))
+ (skip-chars-backward " \t")
+ (point))
+ (treesit-node-start comment-node))))
+ (overlay-put ov 'yaml-pro-format-insert " ")
+ (push ov del-ovs)))))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--expand-long-flow-sequence ()
+ "Expand flow sequence to multiple lines if extend past column 80.
+Assumes that flow sequences have been previously reduced to one line."
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((flow_sequence) @seq)))
+ (ovs '()))
+ (pcase-dolist (`(_ . ,node) nodes)
+ (when (save-excursion (goto-char (treesit-node-end node))
+ (and (looking-at-p ",? *\n")
+ (> (current-column) yaml-pro-format-print-width)))
+ (let* ((capture (treesit-query-capture
+ node
+ '((flow_sequence (flow_node) @child)))))
+ (pcase-dolist (`(_ . ,child) capture)
+ (when (treesit-node-eq (treesit-node-parent child) node)
+ (let* ((indent (yaml-pro-format-ts--node-indent child))
+ (ov (make-overlay
+ (treesit-node-start child) (treesit-node-start child))))
+ (overlay-put
+ ov 'yaml-pro-format-insert
+ (concat "\n" (make-string (* indent yaml-pro-indent) ?\s)))
+ (push ov ovs)))))
+ (pcase-dolist (`(_ . ,close) (treesit-query-capture
+ node
+ '((flow_sequence "]" @close :anchor))))
+ (when (treesit-node-eq (treesit-node-parent close) node)
+ (let* ((ov (make-overlay (treesit-node-start close)
+ (treesit-node-start close)))
+ (indent (yaml-pro-format-ts--node-indent close))
+ (end-str "\n"))
+ (when (not (save-excursion (goto-char (treesit-node-start close))
+ (looking-back ", *" (- (point) 80))))
+ (setq end-str ",\n"))
+ (overlay-put ov 'yaml-pro-format-insert
+ (concat end-str
+ (make-string (* indent yaml-pro-indent)
+ ?\s)))
+ (push ov ovs))))))
+ ovs))
+
+(defun yaml-pro-format-ts--expand-long-flow-mapping ()
+ "Expand flow mapping to multiple lines if extend past column 80.
+Assumes that flow mappings have been previously reduced to one line."
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((flow_mapping) @map)))
+ (ovs '()))
+ (pcase-dolist (`(_ . ,node) nodes)
+ (when (save-excursion
+ (goto-char (treesit-node-end node))
+ (and (looking-at-p ",? *\n")
+ (or (> (current-column) yaml-pro-format-print-width)
+ ;; if the flow is already spanning multiple lines, expand
+ ;; it so it looks nice.
+ (not (= (line-number-at-pos (treesit-node-start node))
+ (line-number-at-pos (treesit-node-end node)))))))
+ (let* ((capture (treesit-query-capture
+ node
+ '((flow_mapping [(flow_pair) (flow_node)] @child)))))
+ (pcase-dolist (`(_ . ,child) capture)
+ (when (and (treesit-node-eq (treesit-node-parent child) node)
+ (save-excursion ;; dont add newline if one already exists
+ (goto-char (treesit-node-start child))
+ (not (looking-back "\n *" (- (point) 80)))))
+ (let* ((indent (yaml-pro-format-ts--node-indent child))
+ (ov (make-overlay (treesit-node-start child)
+ (treesit-node-start child))))
+ (overlay-put ov 'yaml-pro-format-insert
+ (concat "\n" (make-string (* indent yaml-pro-indent)
+ ?\s)))
+ (push ov ovs)))))
+ (let* ((capture (treesit-query-capture
+ node
+ '((flow_mapping "}" @close :anchor)))))
+ (pcase-dolist (`(_ . ,close) capture)
+ (when (treesit-node-eq (treesit-node-parent close) node)
+ (let* ((ov (make-overlay (save-excursion
+ (goto-char (treesit-node-start close))
+ (skip-chars-backward " ")
+ (point))
+ (treesit-node-start close)))
+ (indent (yaml-pro-format-ts--node-indent close))
+ (end-str "\n"))
+ ;; we only add this ov if flow is on same line
+ (unless (save-excursion (goto-char (treesit-node-start close))
+ (looking-back "\n *" (- (point) 80)))
+ (when (not (save-excursion (goto-char (treesit-node-start close))
+ (looking-back ", *" (- (point) 10))))
+ (setq end-str ",\n"))
+
+ (overlay-put
+ ov 'yaml-pro-format-insert
+ (concat end-str (make-string (* indent yaml-pro-indent) ?\s)))
+ (push ov ovs))))))))
+ ovs))
+
+(defun yaml-pro-format-ts--block-sequence ()
+ "Ensure proper spacing around sequences' - character."
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ `((block_sequence_item "-" @dash (_) @elt))))
+ (groups (seq-group-by
+ (lambda (capture)
+ (treesit-node-parent (cdr capture)))
+ nodes))
+ (del-ovs '()))
+ (while groups
+ (save-excursion
+ (let* ((dash-node (alist-get 'dash (cdar groups)))
+ (elt-node (alist-get 'elt (cdar groups))))
+ (let ((ov (make-overlay (treesit-node-end dash-node)
+ (treesit-node-start elt-node))))
+ (overlay-put ov 'yaml-pro-format-insert " ")
+ (push ov del-ovs)))
+ (setq groups (cdr groups))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--single-to-double ()
+ "Convert single quote if it doesn't contain backslashes."
+ (let* ((nodes (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((single_quote_scalar) @scalar)))
+ (del-ovs '()))
+ (pcase-dolist (`(_ . ,node) nodes)
+ (save-excursion
+ (save-restriction
+ (narrow-to-region (treesit-node-start node) (treesit-node-end node))
+ (goto-char (treesit-node-start node))
+ (unless (search-forward "\\" nil t)
+ ;; single quotes are to use backslash w/o escaping. If
+ ;; single scalar has backslash, don't convert.
+ (while (search-forward "''" nil t)
+ (let* ((ov (make-overlay (match-beginning 0) (match-end 0))))
+ (overlay-put ov 'yaml-pro-format-insert "'")
+ (push ov del-ovs)))
+ (let* ((open-ov (make-overlay (treesit-node-start node)
+ (1+ (treesit-node-start node))))
+ (close-ov (make-overlay (1- (treesit-node-end node))
+ (treesit-node-end node))))
+ (overlay-put open-ov 'yaml-pro-format-insert "\"")
+ (overlay-put close-ov 'yaml-pro-format-insert "\"")
+ (push open-ov del-ovs)
+ (push close-ov del-ovs))))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--reduce-spaces ()
+ "Remove multiple spaces when not affecting meaning."
+ (let* ((del-ovs '()))
+ (save-excursion
+ (save-match-data
+ (goto-char (point-min))
+ (while (search-forward-regexp "[ \t][ \t]+" nil t)
+ (when (not (looking-back "\n *" (- (point) 80)))
+ (let* ((at-node-type (treesit-node-type (treesit-node-on
+ (match-beginning 0)
+ (match-end 0)
+ 'yaml))))
+ (when (not (member at-node-type
+ '("string_scalar" "double_quote_scalar"
+ "single_quote_scalar" "block_scalar"
+ "block_sequence_item" "comment")))
+ (let* ((ov (make-overlay (match-beginning 0) (match-end 0))))
+
+ (overlay-put ov 'yaml-pro-format-insert " ")
+ (push ov del-ovs))))))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--after-keep-p (point)
+ "Return non-nil of POINT is after a block scalar keep item.
+This is needed due to a mismatch of the TreeSitter grammer and the YAML spec."
+ (save-excursion
+ (save-match-data
+ (goto-char point)
+ (while (looking-back "[\n ]" (- (point) 2))
+ (forward-char -1))
+ (let* ((at-node (treesit-node-at (point) 'yaml)))
+ (and (equal (treesit-node-type at-node)
+ "block_scalar")
+ (yaml-pro-format-ts--node-keep-block-scalar-p at-node))))))
+
+(defun yaml-pro-format-ts--reduce-newlines ()
+ "Reduce all appropriate grouped empty lines into one."
+ (let* ((del-ovs '()))
+ (save-excursion
+ (save-match-data
+ (goto-char (point-min))
+ (while (search-forward-regexp "\\(?:\n[ \t]*\\)\\(?:\n[ \t]*\\)+\n" nil t)
+ (let* ((on-node-type
+ (treesit-node-type (treesit-node-on (match-beginning 0)
+ (match-end 0)
+ 'yaml))))
+ (when (and (not (member
+ on-node-type '("string_scalar" "double_quote_scalar"
+ "single_quote_scalar" "block_scalar")))
+ (not (yaml-pro-format-ts--after-keep-p (point))))
+ (let* ((ov (make-overlay (match-beginning 0 ) (match-end 0))))
+ (overlay-put ov 'yaml-pro-format-insert "\n\n")
+ (push ov del-ovs)))))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--document-separator-own-line ()
+ "If document contans ---, make sure it's on its own line."
+ (let* ((del-ovs '())
+ (capture (treesit-query-capture (treesit-buffer-root-node 'yaml)
+ '((document "---" @dashes)))))
+ (pcase-dolist (`(_ . ,node) capture)
+ (save-excursion
+ (save-match-data
+ (goto-char (treesit-node-end node))
+ (when (looking-at "\\( *\\)[^ \n]")
+ (let* ((ov (make-overlay (match-beginning 1) (match-end 1))))
+ (overlay-put ov 'yaml-pro-format-insert "\n")
+ (push ov del-ovs))))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--node-numbered-block-scalar-p (node)
+ "Return non-nil if NODE is a block scalar with an indent count."
+ (and (equal (treesit-node-type node) "block_scalar")
+ (string-match-p "^[>|][0-9]+" (treesit-node-text node))))
+
+(defun yaml-pro-format-ts--node-keep-block-scalar-p (node)
+ "Return non-nil if NODE is a block scalar with an indent count."
+ (and (equal (treesit-node-type node) "block_scalar")
+ (string-match-p "^[>|][0-9]*\\+" (treesit-node-text node))))
+
+(defun yaml-pro-format-ts--lowest-block-scalar-indent (node)
+ "Return the lowest indentation of text in NODE."
+ (save-match-data
+ (apply #'min
+ (cdr (seq-map (lambda (line)
+ (when (string-match "^ *" line)
+ (length (match-string 0 line))))
+ (string-lines (treesit-node-text node) t))))))
+
+(defun yaml-pro-format-ts--commenting-next-node (node)
+ "If NODE is a comment, return the node next in the tree if commenting it.
+Often times a comment block will be commenting on something below
+it. We infer this if a comment is part of a block where there is
+a blank line above it."
+ (when (equal (treesit-node-type node) "comment")
+ (let* ((comment-block-start node)
+ (comment-block-end node))
+ (catch 'out
+ (while t
+ (let* ((next-sib (treesit-node-next-sibling comment-block-end)))
+ (if (and (equal (treesit-node-type next-sib) "comment")
+ (= (line-number-at-pos (treesit-node-start next-sib))
+ (1+ (line-number-at-pos (treesit-node-start comment-block-end)))))
+ (setq comment-block-end next-sib)
+ (throw 'out nil)))))
+ (catch 'out
+ (while t
+ (let* ((prev-sib (treesit-node-prev-sibling comment-block-start)))
+ (if (and (equal (treesit-node-type prev-sib) "comment")
+ (= (line-number-at-pos (treesit-node-start prev-sib))
+ (1- (line-number-at-pos (treesit-node-start comment-block-start)))))
+ (setq comment-block-start prev-sib)
+ (throw 'out nil)))))
+ (when (save-excursion
+ (goto-char (treesit-node-start comment-block-start))
+ (forward-line -1)
+ (looking-at-p " *\n"))
+ (let* ((next-block-node
+ (save-excursion
+ (goto-char (treesit-node-start comment-block-end))
+ (forward-line 1)
+ (back-to-indentation)
+ (and (looking-at-p " *[^ \n\t-]")
+ (treesit-node-at (point) 'yaml)))))
+ (when (not (equal (treesit-node-type next-block-node) "comment"))
+ next-block-node))))))
+
+(defun yaml-pro-format-ts--node-indent (root-node)
+ "Return the number of indent parents of ROOT-NODE."
+ (let* ((ct 0)
+ (node root-node))
+ ;; base count calculation
+ (while node
+ (when (and (equal (treesit-node-field-name node) "key")
+ (not (equal (treesit-node-type (treesit-node-parent node))
+ "flow_pair")))
+ (cl-decf ct))
+ (when (member (treesit-node-type node) '("-" "{" "[" "}" "]"))
+ (cl-decf ct))
+ (when (member (treesit-node-type node)
+ '("block_mapping_pair" "block_sequence_item"
+ "flow_mapping" "flow_sequence"))
+ (cl-incf ct))
+ (setq node (treesit-node-parent node)))
+
+ ;; When a non-numbered block scalar is at indent level-0, indent
+ ;; body contents.
+ (when (and (= 0 ct)
+ (equal (treesit-node-type root-node) "block_scalar")
+ (not (yaml-pro-format-ts--node-numbered-block-scalar-p root-node)))
+ (cl-incf ct))
+
+ ;; Block scalars' text that is past the most indent needs to keep
+ ;; its extra indentation
+ (when (and (equal (treesit-node-type root-node) "block_scalar")
+ (> (current-column)
+ (yaml-pro-format-ts--lowest-block-scalar-indent root-node)))
+ (cl-incf ct (/ (- (current-column)
+ (yaml-pro-format-ts--lowest-block-scalar-indent root-node))
+ yaml-pro-indent)))
+ ct))
+
+(defun yaml-pro-format-ts--should-indent-p (node)
+ "Return non-nil if NODE should be indented according to tree parse."
+ (cond
+ ((yaml-pro-format-ts--node-numbered-block-scalar-p node) nil)
+ ((equal (treesit-node-type node) ":") nil)
+ (t t)))
+
+(defun yaml-pro-format-ts--indent ()
+ "Indent elements according to tree position."
+ (let* ((del-ovs '()))
+ (save-excursion
+ (save-match-data
+ (goto-char (point-min))
+ (while (search-forward-regexp "\\(\n *\\)[^ \n]" nil t)
+ (forward-char -1)
+ (let* ((at-node (treesit-node-at (point) 'yaml)))
+ (when (yaml-pro-format-ts--should-indent-p at-node)
+ ;; in certain cases, a comment should be indented by the node
+ ;; after it.
+ (setq at-node (or (yaml-pro-format-ts--commenting-next-node at-node)
+ at-node))
+ (let* ((indent (yaml-pro-format-ts--node-indent at-node))
+ (indent-str
+ (make-string (* indent yaml-pro-indent) ?\s))
+ (ov (make-overlay (1+ (match-beginning 0)) (point))))
+ (overlay-put ov 'yaml-pro-format-insert indent-str)
+ (push ov del-ovs)))))))
+ del-ovs))
+
+(defun yaml-pro-format-ts--process-overlay (ov)
+ "Delete characters in OV. Insert text of property `yaml-pro-format-insert'.
+OV is deleted after this function finishes."
+ (let* ((insert-text (overlay-get ov 'yaml-pro-format-insert)))
+ (goto-char (overlay-start ov))
+ (delete-region (overlay-start ov) (overlay-end ov))
+ (when insert-text
+ (insert insert-text))
+ (delete-overlay ov)))
+
+(defun yaml-pro-format-ts--run-while-changing (functions)
+ "Run FUNCTIONS in order. Keep running FUNCTIONS until no change made."
+ (catch 'done
+ (while t
+ (let ((changed nil))
+ (dolist (f functions)
+ (let* ((ovs (funcall f)))
+ (when (> (length ovs) 0)
+ (setq changed t))
+ (save-excursion
+ (seq-map #'yaml-pro-format-ts--process-overlay ovs))))
+ (if (not changed)
+ (throw 'done nil)
+ (setq changed nil))))))
+
+(defvar-local yaml-pro-format-ts-indent-groups '()
+ "List of nodes that need to be indented together at the same time.")
+
+(defun yaml-pro-format-ts--create-indent-groups ()
+ "Populate `yaml-pro-format-ts-indent-groups' based on initial tree."
+ (setq yaml-pro-format-ts-indent-groups nil)
+ (let* ((capture (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((block_sequence_item "-") @item)))
+ (groups (seq-group-by (pcase-lambda (node)
+ (treesit-node-parent node))
+ (seq-map #'cdr capture))))
+ (pcase-dolist (`(_ . ,items) groups)
+ (when (> (length items) 1)
+ (push (seq-map
+ (pcase-lambda (node)
+ (make-overlay (treesit-node-start node)
+ (treesit-node-end node) nil t))
+ items)
+ yaml-pro-format-ts-indent-groups))))
+ (let* ((capture (treesit-query-capture
+ (treesit-buffer-root-node 'yaml)
+ '((block_mapping_pair ":") @item)))
+ (groups (seq-group-by (pcase-lambda (node)
+ (treesit-node-parent node))
+ (seq-map #'cdr capture))))
+ (pcase-dolist (`(_ . ,items) groups)
+ (when (> (length items) 1)
+ (push (seq-map (pcase-lambda (node)
+ (make-overlay (treesit-node-start node)
+ (treesit-node-end node) nil t))
+ items)
+ yaml-pro-format-ts-indent-groups)))))
+
+(defun yaml-pro-format-ts--ensure-indent-groups ()
+ "Ensure that indent groups are on same level of indentation."
+ (let* ((ovs '()))
+ (dolist (group yaml-pro-format-ts-indent-groups)
+ (let* ((indent (save-excursion (goto-char (overlay-start (car group)))
+ (current-column))))
+ (dolist (node (cdr group))
+ (let* ((node-indent (save-excursion (goto-char (overlay-start node))
+ (current-column)))
+ (indent-diff (- indent node-indent)))
+ (cond
+ ((< indent-diff 0)
+ (let* ((ov (make-overlay (+ (overlay-start node) indent-diff)
+ (overlay-start node))))
+ (push ov ovs)))
+ ((< 0 indent-diff)
+ (let* ((ov (make-overlay (overlay-start node)
+ (overlay-start node))))
+ (overlay-put ov 'yaml-pro-format-insert
+ (make-string indent-diff ?\s))
+ (push ov ovs))))))))
+ ovs))
+
+(defconst yaml-pro-format-ts-functions
+ '((reduce-newlines . yaml-pro-format-ts--reduce-newlines)
+ (document-separator-own-line . yaml-pro-format-ts--document-separator-own-line)
+ (oneline-flow . yaml-pro-format-ts--oneline-flow)
+ (block-formatting . yaml-pro-format-ts--bm-single-space)
+ ((or oneline-flow expand-flow) . yaml-pro-format-ts--flow-seq-grouping-space)
+ (reduce-spaces . yaml-pro-format-ts--reduce-spaces)
+ (block-formatting . yaml-pro-format-ts--block-sequence)
+ (bm-fn-next-line . yaml-pro-format-ts--bm-fn-next-line)
+ (clean-doc-end . yaml-pro-format-ts--clean-doc-end)
+ (remove-spaces-before-comments . yaml-pro-format-ts--remove-spaces-before-comments)
+ (expand-long-flow .
+ (lambda ()
+ (yaml-pro-format-ts--run-while-changing
+ '(yaml-pro-format-ts--expand-long-flow-sequence
+ yaml-pro-format-ts--expand-long-flow-mapping))))
+ (single-to-double .
+ yaml-pro-format-ts--single-to-double)
+ ;; yaml-pro-format-ts--prose-wrap TODO
+ (indent . yaml-pro-format-ts--indent))
+ "Alist of feature symbol and formatting functions.")
+
+
+(defun yaml-pro-format ()
+ "Format the YAML buffer according to pre-defined rules."
+ (interactive)
+ (when (treesit-query-capture (treesit-buffer-root-node 'yaml) '((ERROR) @err))
+ (user-error "Error encountered when parsing buffer with treesitter. Please ensure your syntax is correct and try again"))
+ (save-excursion
+ (goto-char (point-max))
+ (insert "\n"))
+ (yaml-pro-format-ts--create-indent-groups)
+ (let* ((fmt-functions
+ (seq-map
+ #'cdr
+ (seq-filter
+ (pcase-lambda (`(,sym . _))
+ (pcase sym
+ (`(or . ,elts)
+ (seq-find (lambda (sym) (member sym yaml-pro-format-features))
+ elts))
+ (sym (member sym yaml-pro-format-features))))
+ yaml-pro-format-ts-functions))))
+ (dolist (f fmt-functions)
+ (let* ((ovs (funcall f)))
+ (save-excursion
+ (seq-map #'yaml-pro-format-ts--process-overlay ovs)))
+ (let* ((ovs (yaml-pro-format-ts--ensure-indent-groups)))
+ (save-excursion
+ (seq-map #'yaml-pro-format-ts--process-overlay ovs))))
+ (delete-trailing-whitespace (point-min) (point-max))))
+
+(defalias 'yaml-pro-format-ts #'yaml-pro-format)
+
+(provide 'yaml-pro-format)
+
+;;; yaml-pro-format.el ends here
--- /dev/null
+;; -*- no-byte-compile: t; lexical-binding: nil -*-
+(define-package "yaml-pro" "20241223.1723"
+ "Parser-aided YAML editing features."
+ '((emacs "26.1")
+ (yaml "0.5.1"))
+ :url "https://github.com/zkry/yaml-pro"
+ :commit "f7706ea170de98d29b7cdd5a4f189bd038b1e27b"
+ :revdesc "f7706ea170de"
+ :keywords '("tools"))
--- /dev/null
+;;; yaml-pro.el --- Parser-aided YAML editing features -*- lexical-binding: t -*-
+
+;; Author: Zachary Romero
+;; Maintainer: Zachary Romero
+;; Package-Version: 20241223.1723
+;; Package-Revision: f7706ea170de
+;; Package-Requires: ((emacs "26.1") (yaml "0.5.1"))
+;; Homepage: https://github.com/zkry/yaml-pro
+;; Keywords: tools
+
+;; This file is not part of GNU Emacs
+
+;; 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:
+
+;; yaml-pro contains a new mode which provides conveniences when
+;; editing YAML. Running `yaml-pro-mode' switches the mode and the
+;; following commands are available:
+;; - `yaml-pro-move-subtree-up' and `yaml-pro-move-subtree-down'
+;; - `yaml-pro-next-subtree' and `yaml-pro-prev-subtree',
+;; - `yaml-pro-kill-subtree'
+;; - `yaml-pro-up-level'
+;; - `yaml-pro-fold-at-point' and `yaml-pro-unfold-at-point'.
+;; - `yaml-pro-edit-scalar'
+;; - `yaml-pro-jump' or if consult exists `yaml-pro-consult-jump'
+
+;;; Code:
+
+(require 'yaml)
+(require 'yaml-pro-edit)
+(require 'yaml-pro-format)
+(require 'treesit nil t)
+(require 'consult nil t)
+
+(defgroup yaml-pro nil
+ "YAML editing tools."
+ :prefix "yaml-pro-"
+ :group 'convenience)
+
+
+;;; yaml-pro tree sitter
+
+(defcustom yaml-pro-ts-yank-subtrees t
+ "Non-nil means when yanking subtrees, adjust the level."
+ :group 'yaml-pro
+ :type 'boolean)
+
+(defcustom yaml-pro-ts-path-element-separator ?.
+ "Character separating path elements when generating node labels."
+ :group 'yaml-pro
+ :type 'character)
+
+(defun yaml-pro-ts--until-mapping (node)
+ "Recursively look up from NODE returning first `block_mapping_pair'."
+ (treesit-parent-until
+ node
+ (lambda (node)
+ (equal (treesit-node-type node) "block_mapping_pair"))))
+
+(defun yaml-pro-ts--until-mapping-or-list (node)
+ "Recursively look up from NODE returning first `block_mapping_pair'."
+ (treesit-parent-until
+ node
+ (lambda (node)
+ (or (equal (treesit-node-type node) "block_mapping_pair")
+ (equal (treesit-node-type node) "block_sequence_item")))))
+
+(defun yaml-pro-ts--until-list (node)
+ "Recursively look up from NODE returning first `block_sequence_item'."
+ (treesit-parent-until
+ node
+ (lambda (node)
+ (equal (treesit-node-type node) "block_sequence_item"))))
+
+(defun yaml-pro-ts--until-mapping-or-list (node)
+ "Recursively look up from NODE returning first mapping or sequence item."
+ (treesit-parent-until
+ node
+ (lambda (node)
+ (or
+ (equal (treesit-node-type node) "block_mapping_pair")
+ (equal (treesit-node-type node) "block_sequence_item")))))
+
+(defun yaml-pro-ts-kill-subtree ()
+ "Kill the entire subtree located at the current-point."
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (tree-top (yaml-pro-ts--until-mapping-or-list at-node)))
+ (when tree-top
+ (kill-region (treesit-node-start tree-top)
+ (treesit-node-end tree-top)))))
+
+(defun yaml-pro-copy-node-path-at-point ()
+ "Copy node path at point to clipboard."
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (path (yaml-pro-ts--imenu-node-label at-node)))
+ (kill-new path)
+ (message "%s was copied to clipboard" (propertize path 'face '(bold default)))))
+
+(defun yaml-pro-ts-up-level ()
+ "Move the point to the parent tree."
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (tree-top (yaml-pro-ts--until-mapping-or-list at-node))
+ (parent-tree-top (and tree-top (yaml-pro-ts--until-mapping-or-list tree-top))))
+ (when parent-tree-top
+ (goto-char (treesit-node-start parent-tree-top)))))
+
+(defun yaml-pro-ts-down-level ()
+ "Move the point down to first child of current item."
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (tree-top (yaml-pro-ts--until-mapping-or-list at-node))
+ (nodes (seq-map #'cdr
+ (treesit-query-capture tree-top '((block_sequence_item) @match
+ (block_mapping_pair) @match))))
+ (children (seq-sort-by
+ #'treesit-node-start
+ #'<
+ (seq-filter
+ (lambda (node)
+ (treesit-node-eq tree-top (yaml-pro-ts--until-mapping-or-list node)))
+ nodes))))
+ (if (car children)
+ (goto-char (treesit-node-start (car children)))
+ (beep))))
+
+(defun yaml-pro-ts-first-sibling ()
+ "Move the point to the first sibling."
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (at-sibling (yaml-pro-ts--until-mapping-or-list at-node))
+ (parent (treesit-node-parent at-sibling))
+ (all-siblings (treesit-node-children parent))
+ (first-sibling (car all-siblings)))
+ (if first-sibling
+ (goto-char (treesit-node-start first-sibling))
+ (beep))))
+
+(defun yaml-pro-ts-last-sibling ()
+ "Move the point to the last sibling."
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (at-sibling (yaml-pro-ts--until-mapping-or-list at-node))
+ (parent (treesit-node-parent at-sibling))
+ (all-siblings (treesit-node-children parent))
+ (last-sibling (car (last all-siblings))))
+ (if last-sibling
+ (goto-char (treesit-node-start last-sibling))
+ (beep))))
+
+(defun yaml-pro-ts-prev-mapping-node (tree-top type)
+ "Return nearest previous sibling node of TREE-TOP of type TYPE."
+ (interactive)
+ (setq tree-top (treesit-node-prev-sibling tree-top))
+ (while (and tree-top
+ (not (equal (treesit-node-type tree-top) type)))
+ (setq tree-top (treesit-node-prev-sibling tree-top)))
+ tree-top)
+
+(defun yaml-pro-ts-next-mapping-node (tree-top type)
+ "Return nearest next sibling node of TREE-TOP of type TYPE."
+ (interactive)
+ (setq tree-top (treesit-node-next-sibling tree-top))
+ (while (and tree-top
+ (not (equal (treesit-node-type tree-top) type)))
+ (setq tree-top (treesit-node-next-sibling tree-top)))
+ tree-top)
+
+
+(defun yaml-pro-ts-prev-subtree ()
+ "Move the point to the previous subtree or list."
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (prev-node
+ (if (and (equal (treesit-node-type at-node) "-")
+ (equal (treesit-node-type (treesit-node-parent at-node))
+ "block_sequence_item"))
+ (yaml-pro-ts-prev-mapping-node (yaml-pro-ts--until-list at-node) "block_sequence_item")
+ (yaml-pro-ts-prev-mapping-node (yaml-pro-ts--until-mapping at-node) "block_mapping_pair"))))
+ (if prev-node
+ (goto-char (treesit-node-start prev-node))
+ (ding))))
+
+(defun yaml-pro-ts-backward-sentence ()
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (starting-point (point)))
+ (unless (bobp)
+ (cond
+ ((or (equal (treesit-node-type at-node) "\"")
+ (equal (treesit-node-type at-node) "'")
+ (equal (treesit-node-type at-node) "block_scalar"))
+ (let ((sentence-end "\\. "))
+ (backward-sentence)
+ (when (not (equal
+ (treesit-node-type at-node)
+ (treesit-node-type (treesit-node-at (point) 'yaml))))
+ (goto-char (treesit-node-start at-node))
+ (forward-line -1)
+ (yaml-pro-ts-backward-sentence))))
+ (t
+ (let ((ring-bell-function #'ignore))
+ (forward-line -1)
+ (while (looking-at "^ *\\(#.*\\)?$")
+ (forward-line -1))
+ (when (not (bobp))
+ (cl-flet ((move-to-leaf ()
+ (let ((start-point (point)))
+ (catch 'done
+ (while t
+ (yaml-pro-ts-down-level)
+ (when (eql (point) start-point)
+ (throw 'done t))
+ (setq start-point (point))))
+ (goto-char (pos-eol)))))
+ (let ((start (point)))
+ (while (progn (move-to-leaf)
+ (>= (point) starting-point))
+ (goto-char start)
+ (forward-line -1)
+ (while (and (looking-at "^ *\\(#.*\\)?$")
+ (not (bobp)))
+ (forward-line -1))
+ (setq start (point))))))))))))
+
+(defun yaml-pro-ts-forward-sentence ()
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml)))
+ (cond
+ ((or (equal (treesit-node-type at-node) "\"")
+ (equal (treesit-node-type at-node) "'")
+ (equal (treesit-node-type at-node) "block_scalar"))
+ (let ((sentence-end "\\. "))
+ (forward-sentence)
+ (when (not (equal
+ (treesit-node-type at-node)
+ (treesit-node-type (treesit-node-at (point) 'yaml))))
+ (goto-char (treesit-node-end at-node))
+ (forward-line 1)
+ (yaml-pro-ts-forward-sentence))))
+ (t
+ (let ((ring-bell-function #'ignore))
+ (forward-line 1)
+ (while (and (looking-at "^ *\\(#.*\\)?$")
+ (not (eobp)))
+ (forward-line 1))
+ (let ((start-point (point)))
+ (catch 'done
+ (while t
+ (yaml-pro-ts-down-level)
+ (when (or (eql (point) start-point)
+ (eobp))
+ (throw 'done t))
+ (setq start-point (point))))
+ (goto-char (pos-eol))))))))
+
+(defun yaml-pro-ts-next-subtree ()
+ "Move the point to the next subtree or list."
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (next-node
+ (if (and (equal (treesit-node-type at-node) "-")
+ (equal (treesit-node-type (treesit-node-parent at-node))
+ "block_sequence_item"))
+ (yaml-pro-ts-next-mapping-node (yaml-pro-ts--until-list at-node) "block_sequence_item")
+ (yaml-pro-ts-next-mapping-node (yaml-pro-ts--until-mapping at-node) "block_mapping_pair"))))
+ (if next-node
+ (goto-char (treesit-node-start next-node))
+ (ding))))
+
+(defun yaml-pro-ts-move-subtree (dir)
+ "Get the current and DIR node and swap the contents of the two."
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (tree-top (yaml-pro-ts--until-mapping-or-list at-node))
+ (tree-top-type (treesit-node-type tree-top))
+ (sibling-node (if (eq dir 'down)
+ (yaml-pro-ts-next-mapping-node tree-top tree-top-type)
+ (yaml-pro-ts-prev-mapping-node tree-top tree-top-type)))
+ (at-start-marker (make-marker))
+ (at-end-marker (make-marker))
+ (sibling-start-marker (make-marker))
+ (sibling-end-marker (make-marker))
+ (sibling-end-trailing-newline nil))
+ (when (and tree-top sibling-node)
+ (set-marker at-start-marker (treesit-node-start tree-top))
+ (set-marker at-end-marker (treesit-node-end tree-top))
+ (set-marker sibling-start-marker (treesit-node-start sibling-node))
+ (set-marker sibling-end-marker (treesit-node-end sibling-node))
+ ;; Check if there's a trailing newline after the last node. We
+ ;; do this because we don't want to insert a trailing newline if
+ ;; the user doesn't have a trailing newline.
+ (save-excursion
+ (goto-char sibling-end-marker)
+ (setq sibling-end-trailing-newline (equal (char-before) ?\n)))
+ (let* ((at-tree-text (buffer-substring-no-properties
+ at-start-marker at-end-marker))
+ (sibling-tree-text (buffer-substring-no-properties
+ sibling-start-marker sibling-end-marker)))
+ (delete-region at-start-marker at-end-marker)
+ (delete-region sibling-start-marker sibling-end-marker)
+ (goto-char sibling-start-marker)
+ ;; Empty newlines that are after the last node are moved along
+ ;; with the last node, so after text has been deleted from
+ ;; at-node and sibling-node, there's no newline for
+ ;; at-node. We only do this when the last item has a trailing
+ ;; newline.
+ (when (and (eobp) sibling-end-trailing-newline)
+ (save-excursion
+ (insert "\n")))
+ (insert at-tree-text)
+ (goto-char at-start-marker)
+ ;; We delete the last newline (if there's one) because when we
+ ;; move to `at-start-marker', there's already an empty line.
+ (insert (replace-regexp-in-string "\n\\'" "" sibling-tree-text))
+ (goto-char sibling-start-marker)))))
+
+(defun yaml-pro-ts-move-subtree-up ()
+ "Swap the current tree with that of the next sibling's."
+ (interactive)
+ (yaml-pro-ts-move-subtree 'up))
+
+(defun yaml-pro-ts-move-subtree-down ()
+ "Swap the current tree with that of the previous sibling's."
+ (interactive)
+ (yaml-pro-ts-move-subtree 'down))
+
+(defun yaml-pro-ts-meta-return ()
+ "Insert new list item after current item."
+ (interactive)
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (list-item-top (yaml-pro-ts--until-list at-node))
+ (list-item-end-in-newline nil))
+ (when list-item-top
+ (let* ((indentation (save-excursion
+ (goto-char (treesit-node-start list-item-top))
+ (current-column))))
+ (goto-char (treesit-node-end list-item-top))
+ ;; If the file didn't have a newline at the end of the item,
+ ;; then we are looking at ^$
+ (setq list-item-end-in-newline (looking-at "^$"))
+ (cond
+ ;; After visiting the end of the top item, we check if we are
+ ;; in a newline or not. If we are not, we start a new line.
+ ((not (looking-at "^$"))
+ (insert "\n" (make-string indentation ?\s) "- "))
+ ;; When there are empty lines above the point after going to
+ ;; the end of the element. When in the top last element of a
+ ;; list, treesit-node-end considers empty lines as part of
+ ;; the list element.
+ ((save-excursion
+ (forward-line -1)
+ (looking-at "^$"))
+ (while (save-excursion
+ (forward-line -1)
+ (looking-at "^$"))
+ (forward-line -1))
+ (insert (make-string indentation ?\s) "- "))
+ ;; If none of the cases are true, we are at the beginning of
+ ;; a line just one line below the previous item, so we just
+ ;; start another item.
+ (t
+ (insert (make-string indentation ?\s) "- ")))
+ ;; If the the user inserted a newline at the end of the item,
+ ;; we insert a newline. If the user didn't, we don't insert a
+ ;; newline. We do this to conform with the POSIX definition of
+ ;; a line and at the same time to act according the user's
+ ;; preferences.
+ (when list-item-end-in-newline
+ (save-excursion
+ (insert "\n")))))))
+
+(defun yaml-pro-convolute-tree ()
+ "Swap the keys of the parent of the item at point and the parent's parent."
+ (interactive)
+ (let* ((at-node (yaml-pro-ts--until-mapping (treesit-node-at (point) 'yaml)))
+ (parent-tree (yaml-pro-ts--until-mapping at-node))
+ (grandparent-tree (yaml-pro-ts--until-mapping
+ (treesit-node-parent parent-tree))))
+ (when (and parent-tree grandparent-tree)
+ (let* ((query '((block_mapping_pair key: (_) @key)))
+ (parent-key (alist-get 'key (treesit-query-capture parent-tree query)))
+ (parent-start (treesit-node-start parent-key))
+ (parent-end (treesit-node-end parent-key))
+ (parent-text (buffer-substring-no-properties parent-start parent-end))
+ (grandparent-key (alist-get 'key (treesit-query-capture grandparent-tree query)))
+ (grandparent-start (treesit-node-start grandparent-key))
+ (grandparent-end (treesit-node-end grandparent-key))
+ (grandparent-text (buffer-substring-no-properties
+ grandparent-start grandparent-end)))
+ (save-excursion
+ (delete-region parent-start parent-end)
+ (goto-char parent-start)
+ (insert grandparent-text)
+ (delete-region grandparent-start grandparent-end)
+ (goto-char grandparent-start)
+ (insert parent-text))))))
+
+(defun yaml-pro-ts-indent-subtree (prefix)
+ "Indent the current subtree by one level.
+
+This function uses tree-sitter. Indentation is controlled by the
+variable `yaml-pro-indent'."
+ (interactive "p\n")
+ (let* ((at-subtree (yaml-pro-ts--until-mapping (treesit-node-at (point) 'yaml)))
+ (origin-marker (make-marker))
+ (end-marker (make-marker))
+ (indent-str (make-string yaml-pro-indent ?\s))
+ (original-content (concat
+ (buffer-substring-no-properties
+ (save-excursion (goto-char (treesit-node-start at-subtree)) (pos-bol))
+ (treesit-node-start at-subtree))
+ (treesit-node-text at-subtree t)))
+ (original-point (point))
+ (start-point (save-excursion (goto-char (treesit-node-start at-subtree)) (pos-bol))))
+ (set-marker origin-marker (point))
+ (set-marker end-marker (treesit-node-end at-subtree))
+ (goto-char start-point)
+ (forward-line 0)
+ (while (< (point) end-marker)
+ (when (not (looking-at " *$"))
+ (insert indent-str))
+ (forward-line 1))
+ (goto-char origin-marker)
+ (when (and
+ (= prefix 1)
+ (treesit-query-capture (treesit-buffer-root-node 'yaml) '((ERROR) @err)))
+ (delete-region start-point end-marker)
+ (goto-char start-point)
+ (insert original-content)
+ (goto-char original-point)
+ (beep)
+ (message "indent would result in broken tree. Prefix command with `C-u' to force indent."))
+ (set-marker origin-marker nil)
+ (set-marker end-marker nil)))
+
+(defun yaml-pro-ts-unindent-subtree (prefix)
+ "Unindent the current subtree by one level.
+
+This function uses tree-sitter. Indentation is controlled by the
+variable `yaml-pro-indent'."
+ (interactive "p\n")
+ (let* ((at-subtree (yaml-pro-ts--until-mapping (treesit-node-at (point) 'yaml)))
+ (origin-marker (make-marker))
+ (end-marker (make-marker))
+ (indent-str (make-string yaml-pro-indent ?\s))
+ (original-content (concat
+ (buffer-substring-no-properties
+ (save-excursion (goto-char (treesit-node-start at-subtree)) (pos-bol))
+ (treesit-node-start at-subtree))
+ (treesit-node-text at-subtree t)))
+ (original-point (point))
+ (start-point (save-excursion (goto-char (treesit-node-start at-subtree))
+ (pos-bol))))
+ (set-marker origin-marker (point))
+ (set-marker end-marker (treesit-node-end at-subtree))
+ (goto-char start-point)
+ (forward-line 0)
+ (while (< (point) end-marker)
+ (when (looking-at (regexp-quote indent-str))
+ (delete-char yaml-pro-indent))
+ (forward-line 1))
+ (goto-char origin-marker)
+ (when (and
+ (= 1 prefix)
+ (treesit-query-capture (treesit-buffer-root-node 'yaml) '((ERROR) @err)))
+ (delete-region start-point end-marker)
+ (goto-char start-point)
+ (insert original-content)
+ (goto-char original-point)
+ (beep)
+ (message "unindent would result in broken tree. Prefix command with `C-u' to force indent."))
+ (set-marker origin-marker nil)
+ (set-marker end-marker nil)))
+
+(defun yaml-pro-ts-mark-subtree (up)
+ "Mark the current subtree.
+This puts point at the start of the current subtree, and mark at
+the end. If a numeric prefix UP is given, move up into the
+hierarchy of headlines by UP levels before marking the subtree."
+ (interactive "P")
+ (while (and up (> up 0))
+ (yaml-pro-ts-up-level)
+ (cl-decf up))
+ (let* ((at-subtree (yaml-pro-ts--until-mapping-or-list (treesit-node-at (point) 'yaml))))
+ (if (and (not up)
+ (or (and (eq last-command this-command) (mark t))
+ (and transient-mark-mode mark-active)))
+ (progn
+ (while (and at-subtree (<= (treesit-node-end at-subtree) (mark)))
+ (setq at-subtree
+ (yaml-pro-ts-next-mapping-node
+ at-subtree (treesit-node-type at-subtree))))
+ (when at-subtree
+ (set-mark (treesit-node-end at-subtree))))
+ (push-mark (treesit-node-end at-subtree))
+ (goto-char (treesit-node-start at-subtree))
+ (activate-mark))))
+
+(defun yaml-pro-ts--kill-is-subtree (&optional tree)
+ "Return non-nil if TREE (or current kill) is a valid tree."
+ (unless tree
+ (setq tree (and kill-ring (current-kill 0))))
+ (when (and tree (> (length (string-split tree "\n")) 1))
+ (let ((node (treesit-parse-string tree 'yaml)))
+ (treesit-query-capture node '((block_mapping_pair) @key)))))
+
+(defun yaml-pro-ts--kill-is-sequence (&optional tree)
+ "Return non-nil if TREE (or current kill) is a valid sequence."
+ (unless tree
+ (setq tree (and kill-ring (current-kill 0))))
+ (let ((first-line (car (string-split (string-trim-left tree) "\n"))))
+ (when (and tree (> (length (string-split tree "\n")) 1))
+ (let ((node (treesit-parse-string first-line 'yaml)))
+ (treesit-query-capture node '((block_sequence) @key))))))
+
+(defun yaml-pro-ts-paste-sequence (&optional remove)
+ "Insert the current kill into the buffer, preserving sequence structure.
+If REMOVE is non-nil, pop item off `kill-ring'."
+ (interactive)
+ (let ((seq (current-kill 0)))
+ (unless (yaml-pro-ts--kill-is-sequence seq)
+ (user-error
+ (substitute-command-keys
+ "The kill is not a YAML-sequence. Use `\\[yank]' to yank anyways")))
+ (let* ((base-indent (current-column))
+ (seq (string-trim seq))
+ (seq-lines (split-string seq "\n"))
+ (indent-lengths (save-match-data
+ (or (seq-map
+ (lambda (line)
+ (string-match "^\\( *\\)" line)
+ (length (match-string 1 line)))
+ (seq-filter
+ (lambda (line)
+ (string-match-p "^ *-" line))
+ (cdr seq-lines)))
+ (list (* yaml-pro-indent 2)))))
+ (smallest-indent (apply #'min indent-lengths))
+ (smallest-indent-string (make-string smallest-indent ?\s))
+ (seq-rest-lines-at-indent (string-join
+ (seq-map
+ (lambda (line)
+ (concat
+ (make-string base-indent ?\s)
+ (string-remove-prefix smallest-indent-string line)))
+ (cdr seq-lines))
+ "\n")))
+ (push-mark (point) 'nomsg)
+ (insert (car seq-lines) "\n")
+ (insert seq-rest-lines-at-indent))))
+
+(defun yaml-pro-ts-paste-subtree (&optional remove)
+ "Insert the current kill into the buffer, preserving tree structure.
+If REMOVE is non-nil, pop item off `kill-ring'."
+ (interactive)
+ (let ((tree (current-kill 0)))
+ (unless (yaml-pro-ts--kill-is-subtree tree)
+ (user-error
+ (substitute-command-keys
+ "The kill is not a YAML-tree. Use `\\[yank]' to yank anyways")))
+ (let* ((base-indent (current-column))
+ (indent-lengths (save-match-data
+ (seq-map
+ (lambda (line)
+ (string-match "^\\( *\\)" line)
+ (length (match-string 1 line)))
+ (seq-filter
+ (lambda (line)
+ (string-match-p "^ *[a-zA-Z0-9\"'_-]+:" line))
+ (cdr (split-string tree "\n")))))))
+ (if (not indent-lengths)
+ (progn
+ (push-mark (point) 'nomsg)
+ (insert tree))
+ (let* (;; does the killed text have more than one top level?
+ ;; if so we need to calculate indentation differently.
+ (single-top-lvl-p
+ (seq-every-p
+ (lambda (len)
+ (<= (car indent-lengths) len))
+ (cdr indent-lengths)))
+ ;; indent that the rest of kill text should have
+ (kill-indent (max (+ (apply #'min indent-lengths)
+ (if single-top-lvl-p -2 0))
+ 0))
+ (kill-indent-string (make-string kill-indent ?\s))
+ (base-indent-string (make-string base-indent ?\s))
+ (insertion-string (with-temp-buffer
+ (insert tree)
+ (goto-char (point-min))
+ (forward-line 1)
+ (while (not (eobp))
+ (when (looking-at kill-indent-string)
+ (delete-char kill-indent)
+ (insert base-indent-string))
+ (forward-line 1))
+ (buffer-string))))
+ (push-mark (point) 'nomsg)
+ (insert insertion-string)))))
+ (when remove (pop kill-ring)))
+
+(defun yaml-pro-ts-yank (&optional arg)
+ "Yank text on kill ring. If YAML-subtree, then indent it correctly.
+This command will look at the current kill and check if it is a
+subtree, or series of subtrees. If so, and ARG is nil, the
+subtree is yanked with the appropriate amount of whitespace
+inserted to make the tree retain its original structure."
+ (interactive "P")
+ (setq this-command 'yank)
+ (if arg
+ (call-interactively #'yank)
+ (let* ((subtreep (yaml-pro-ts--kill-is-subtree))
+ (seqp (yaml-pro-ts--kill-is-sequence))
+ (pos (point))
+ ;; Is point on a placeholder for a block node?
+ (block-allowed-p
+ (and
+ ;; The line must not have nothing but spaces after the point.
+ (looking-at "[ \t]*$")
+ (and (save-excursion
+ (skip-chars-backward " \t")
+ (or
+ ;; The line is blank.
+ (bolp)
+ ;; The point is preceded by a block struct indicator.
+ ;; Note that compact collections cannot be preceded by
+ ;; anchors or tags.
+ (and (memq (char-before) '(?- ?: ??))
+ ;; Block struct indicators must be followed by spaces.
+ (memq (char-before pos) '(?\s ?\t)))))))))
+ (cond
+ ((and seqp block-allowed-p yaml-pro-ts-yank-subtrees)
+ (yaml-pro-ts-paste-sequence))
+ ((and subtreep block-allowed-p yaml-pro-ts-yank-subtrees)
+ (yaml-pro-ts-paste-subtree))
+ (t (call-interactively #'yank))))))
+
+(defun yaml-pro-ts-newline (&optional arg interactive)
+ (interactive "*P\np")
+ (barf-if-buffer-read-only)
+ (cond
+ ((or (looking-at-p " *[^ \n]")
+ (equal (treesit-node-type (treesit-node-at (point) 'yaml)) ":")
+ (equal (treesit-node-type (treesit-node-parent (treesit-node-at (point) 'yaml)))
+ "block_scalar"))
+ (let* ((indent (yaml-pro-format-ts--node-indent (treesit-node-at (point) 'yaml)))
+ (indent-column (* indent yaml-pro-indent)))
+ (call-interactively #'newline)
+ (when (< (current-column) indent-column)
+ (insert (make-string (- indent-column (current-column)) ?\s)))))
+ (t (call-interactively #'newline))))
+
+(defun yaml-pro-ts--imenu-node-label (node)
+ "Return an imenu label for NODE."
+ (let ((top node)
+ (root (treesit-buffer-root-node 'yaml))
+ (label ""))
+ (while (not (treesit-node-eq node root))
+ (cond
+ ((equal (treesit-node-type node) "block_mapping_pair")
+ (let ((key-node (treesit-node-child-by-field-name node "key")))
+ (when key-node
+ (setq label (concat (treesit-node-text key-node)
+ (if (equal label "") "" (string yaml-pro-ts-path-element-separator))
+ label)))))
+ ((equal (treesit-node-type node) "block_sequence_item")
+ (setq label (format "[%d]%c%s" (treesit-node-index node) yaml-pro-ts-path-element-separator label))))
+ (setq node (treesit-node-parent node)))
+ label))
+
+(defun yaml-pro-ts-create-index ()
+ "Create imenu index of YAML file using treesitter."
+ (let ((all-keys (treesit-query-capture (treesit-buffer-root-node 'yaml) '((block_mapping_pair key: (_) @key))))
+ (imenu-items '()))
+ (dolist (key-item all-keys)
+ (let* ((key-node (cdr key-item))
+ (key-label (yaml-pro-ts--imenu-node-label key-node)))
+ (push (cons key-label (treesit-node-start key-node)) imenu-items)))
+ (seq-reverse imenu-items)))
+
+(defun yaml-pro-ts-eldoc (&rest _ignored)
+ "Return eldoc message of current point."
+ (let* ((at-tree (yaml-pro-ts--until-mapping-or-list (treesit-node-at (point) 'yaml))))
+ (when at-tree
+ (yaml-pro-ts--imenu-node-label at-tree))))
+
+(defun yaml-pro-ts--path-to-imenu-key (path)
+ "Stringify path to the same format that imenu uses.
+E.g. the path (\"one\" 0 \"two\") results in the key string \"one.[0].two\"."
+ (string-join
+ (seq-map
+ (lambda (elt) (if (numberp elt) (format "[%d]" elt) elt))
+ path)
+ (string yaml-pro-ts-path-element-separator)))
+
+(defun yaml-pro-ts-jump-to-definition (&rest parts)
+ "Jump to a part of the YAML file based on PARTS.
+If element is a sting, use it as block mapping key.
+If element is a number, use it as a sequence index."
+ (imenu (yaml-pro-ts--path-to-imenu-key parts)))
+
+(defun yaml-pro-ts-add-mapping (&rest keys)
+ "Add KEYS as levels beneath current node's block mapping.
+KEYS are added as increasingly nested levels."
+ (let* ((paths (seq-map #'car (yaml-pro-ts-create-index)))
+ (to-go keys)
+ (to-add'()))
+ (while (and (not (member (yaml-pro-ts--path-to-imenu-key to-go) paths))
+ to-go)
+ (setq to-add (append (last to-go) to-add))
+ (setq to-go (butlast to-go)))
+ (when to-go
+ (apply #'yaml-pro-ts-jump-to-definition to-go))
+ (when to-add
+ (let* ((at-node (treesit-node-at (point) 'yaml))
+ (parent (treesit-parent-until
+ at-node
+ (lambda (node)
+ (equal (treesit-node-type node) "block_mapping_pair"))
+ t))
+ (parent-indent (if to-go
+ (save-excursion
+ (goto-char (treesit-node-start parent))
+ (current-column))
+ (- yaml-pro-indent))))
+ (if (or (not to-go)
+ (or (not (treesit-node-child-by-field-name parent "value"))
+ (equal (treesit-node-type (car (treesit-node-children (treesit-node-child-by-field-name parent "value"))))
+ "block_mapping")))
+ (progn
+ (if to-go
+ (goto-char (treesit-node-end parent))
+ (goto-char (point-max))
+ (unless (looking-back "\n" (- (point) 2))
+ (insert "\n")))
+ (while to-add
+ (insert "\n")
+ (insert (make-string (+ yaml-pro-indent parent-indent) ?\s))
+ (insert (car to-add) ":")
+ (setq to-add (cdr to-add))
+ (cl-incf parent-indent yaml-pro-indent)))
+ (user-error "unable to to insert element at position"))))))
+
+(defconst yaml-pro-ts-mode-map
+ (let ((map (make-sparse-keymap)))
+ (prog1 map
+ (define-key map (kbd "C-c C-x C-w") #'yaml-pro-ts-kill-subtree)
+ (define-key map (kbd "C-c C-u") #'yaml-pro-ts-up-level)
+ (define-key map (kbd "C-c C-d") #'yaml-pro-ts-down-level)
+
+ (define-key map (kbd "C-c C-p") #'yaml-pro-ts-prev-subtree)
+ (define-key map (kbd "C-c C-n") #'yaml-pro-ts-next-subtree)
+
+ (define-key map (kbd "M-a") #'yaml-pro-ts-backward-sentence)
+ (define-key map (kbd "M-e") #'yaml-pro-ts-forward-sentence)
+
+ (define-key map (kbd "s-<up>") #'yaml-pro-ts-move-subtree-up)
+ (define-key map (kbd "s-<down>") #'yaml-pro-ts-move-subtree-down)
+
+ (define-key map (kbd "C-c >") #'yaml-pro-ts-indent-subtree)
+ (define-key map (kbd "C-c <") #'yaml-pro-ts-unindent-subtree)
+
+ (define-key map (kbd "M-<return>") #'yaml-pro-ts-meta-return)
+ (define-key map (kbd "M-?") #'yaml-pro-convolute-tree)
+ (define-key map (kbd "C-c @") #'yaml-pro-ts-mark-subtree)
+ (define-key map (kbd "C-c C-x C-y") #'yaml-pro-ts-paste-subtree)
+
+ (define-key map [remap yank] #'yaml-pro-ts-yank)
+
+ (define-key map (kbd "C-c '") #'yaml-pro-edit-ts-scalar)))
+ "Map for minor mode `yaml-pro-ts-mode'.")
+
+;;;###autoload
+(define-minor-mode yaml-pro-ts-mode
+ "Minor mode to enable yaml-pro treesitter keymap.
+
+\\{yaml-pro-mode-map}"
+ :init-value nil
+ :group 'yaml-pro
+ :keymap yaml-pro-ts-mode-map
+
+ (when yaml-pro-ts-mode
+ (unless (featurep 'treesit)
+ (user-error "Tree-sitter not supported in current Emacs version"))
+ (unless (treesit-ready-p 'yaml)
+ (user-error "YAML tree-sitter not ready"))
+ (treesit-parser-create 'yaml)
+ (setq imenu-generic-expression nil)
+ (setq imenu-create-index-function #'yaml-pro-ts-create-index)
+ (if (boundp 'eldoc-documentation-functions)
+ (add-hook 'eldoc-documentation-functions #'yaml-pro-ts-eldoc nil t)
+ (setq-local eldoc-documentation-function #'yaml-pro-ts-eldoc))))
+
+\f
+;;; yaml-pro parser
+
+(defcustom yaml-pro-max-parse-size 5000
+ "Size of buffer for which any size greater than use heuristic to parse.
+
+Note that this isn't fully compatable with every command."
+ :group 'yaml-pro
+ :type 'number
+ :package-version '(yaml-pro "0.2.0"))
+
+(defface yaml-pro-fold-replacement-face
+ '((t :inherit 'font-lock-comment-face))
+ "Face for fold replacement text.")
+
+(defcustom yaml-pro-indent (if (boundp 'yaml-indent)
+ yaml-indent
+ 2)
+ "Default indentation to use for yaml-pro."
+ :group 'yaml-pro
+ :type 'integer)
+
+(defvar-local yaml-pro-buffer-tree nil)
+
+(defun yaml-pro--get-yaml-position (string)
+ "Return the property `yaml-position' of STRING.
+This function is needed as the parser may insert unpropertized
+spacing before the match."
+ (or (get-text-property 0 'yaml-position string)
+ (get-text-property 0 'yaml-position (string-trim-left string "[ \n]*"))))
+
+(defun yaml-pro--offset-parse-tree (tree offset)
+ "Offset all TREE values of `yaml-position' property by OFFSET."
+ (cond
+ ((or (listp tree) (vectorp tree))
+ (seq-map
+ (lambda (elt)
+ (yaml-pro--offset-parse-tree elt offset))
+ tree))
+ ((stringp tree)
+ (let* ((yaml-position (yaml-pro--get-yaml-position tree)))
+ (when yaml-position
+ (let ((offset-position (cons (+ (car yaml-position) offset)
+ (+ (cdr yaml-position) offset))))
+ (set-text-properties 0
+ (length tree)
+ (list 'yaml-position offset-position)
+ tree)))))))
+
+(defun yaml-pro--use-fast-p ()
+ "Return non nil if buffer size is larger than `yaml-pro-max-parse-size'."
+ (>= (buffer-size) yaml-pro-max-parse-size))
+
+(defun yaml-pro--fast-parse-bounds (&optional point)
+ "Return a list of the start and end point of subsection to parse.
+
+Find subsection based off of POINT if provided."
+ (let* ((point-indent
+ (save-excursion
+ (beginning-of-line)
+ (skip-chars-forward " ")
+ (current-column)))
+ (indent-regexp (make-string point-indent ?\s))
+ (start-point)
+ (end-point))
+ (if (= point-indent 0)
+ (setq start-point (point-min)
+ end-point (point-max))
+ (save-excursion
+ (beginning-of-line)
+ (while (not (or (bobp)
+ (and (looking-at " *[a-zA-Z_0-9-]+:\\s-*$")
+ (not (looking-at indent-regexp)))))
+ (forward-line -1))
+ (setq start-point (point)))
+ (save-excursion
+ (beginning-of-line)
+ (while (not (or (eobp)
+ (and (not (looking-at indent-regexp))
+ (not (looking-at " *$")))))
+ (forward-line 1))
+ (setq end-point (point))))
+ (list start-point end-point)))
+
+(defun yaml-pro--get-buffer-tree ()
+ "Return the cached buffer-tree if exists, else regenerate it."
+ (or yaml-pro-buffer-tree
+ (if (not (yaml-pro--use-fast-p))
+ (let ((tree (yaml-parse-tree (buffer-string))))
+ (setq yaml-pro-buffer-tree tree)
+ tree)
+ (seq-let (start-point end-point) (yaml-pro--fast-parse-bounds)
+ (let* ((subsection-string (buffer-substring start-point end-point))
+ (tree (yaml-parse-tree subsection-string)))
+ (yaml-pro--offset-parse-tree tree (1- start-point))
+ (when (and (= start-point (point-min))
+ (= end-point (point-max)))
+ (setq yaml-pro-buffer-tree tree))
+ tree)))))
+
+(defun yaml-pro--after-change-hook (_ _ _)
+ "Delete cached tree on buffer change."
+ (setq yaml-pro-buffer-tree nil))
+
+(defun yaml-pro--find-node (parse point)
+ "Recursively look through PARSE to find scalar at POINT."
+ (catch 'done
+ (cond
+ ((vectorp parse)
+ (seq-do
+ (lambda (item)
+ (let ((res (yaml-pro--find-node item point)))
+ (when res
+ (throw 'done res))))
+ parse)
+ nil)
+ ((listp parse)
+ (dolist (item parse)
+ (let ((res (yaml-pro--find-node (cdr item) point)))
+ (when res
+ (throw 'done res)))))
+ ((stringp parse)
+ (let* ((bounds (yaml-pro--get-yaml-position parse))
+ (start (and bounds (car bounds)))
+ (end (and bounds (cdr bounds))))
+ (if (and start end (<= start point end))
+ parse
+ nil))))))
+
+(defun yaml-pro--fast-value-at-point ()
+ "Return the scalar under the current point.
+
+This function uses a heuristic to limit the amount of parsing
+that has to be done."
+
+ (seq-let (start-point end-point) (yaml-pro--fast-parse-bounds)
+ (let* ((subsection-string (buffer-substring start-point end-point))
+ (parse (yaml-parse-string-with-pos subsection-string))
+ (val (yaml-pro--find-node parse (1+ (- (point) start-point)))))
+ (when val
+ (let* ((yaml-position (yaml-pro--get-yaml-position val))
+ (new-position (cons (1- (+ (car yaml-position) start-point))
+ (1- (+ (cdr yaml-position) start-point)))))
+ (set-text-properties 0 (length val) (list 'yaml-position new-position) val)
+ val)))))
+
+(defun yaml-pro--value-at-point ()
+ "Return the scalar under the current point."
+ (let* ((parse (yaml-parse-string-with-pos (buffer-string))))
+ (yaml-pro--find-node parse (point))))
+
+(defun yaml-pro--get-parent-block* (tree point)
+ "Return subtree from TREE that best contain POINT."
+ (if (not (listp tree))
+ nil
+ (let ((sub-blocks
+ (seq-filter #'identity
+ (seq-map (lambda (st)
+ (yaml-pro--get-parent-block* st point))
+ tree))))
+ (cond
+ ((and sub-blocks
+ (stringp (car tree))
+ (let* ((bounds (yaml-pro--get-yaml-position (cadr tree)))
+ (start (car bounds))
+ (end (cdr bounds)))
+ (and (numberp start) (<= start point end))))
+ (let* ((bounds (yaml-pro--get-yaml-position (cadr tree)))
+ (start (car bounds))
+ (end (cdr bounds)))
+ (throw 'result (list start end))))
+ (sub-blocks
+ (car sub-blocks))
+ ((stringp (car tree))
+ (let* ((bounds (yaml-pro--get-yaml-position (cadr tree)))
+ (start (and bounds (car bounds)))
+ (end (and bounds (cdr bounds))))
+ (if (and (numberp start) (<= start point end))
+ (list start end)
+ nil)))
+ (t nil)))))
+
+(defun yaml-pro-get-parent-block (tree point)
+ "Return the nearest parent block in TREE to node in POINT."
+ (catch 'result
+ (yaml-pro--get-parent-block* tree point)))
+
+(defun yaml-pro-get-block-bounds (tree point)
+ "Return subtree from TREE that best contain POINT."
+ (if (not (listp tree))
+ nil
+ (let ((sub-blocks
+ (seq-filter #'identity
+ (seq-map (lambda (st) (yaml-pro-get-block-bounds st point))
+ tree))))
+ (cond
+ (sub-blocks
+ ;; TODO should find best match instead of firt (?)
+ (car sub-blocks))
+ ((and (stringp (car tree)) (not (equal (car tree) "")))
+ (let* ((bounds (yaml-pro--get-yaml-position (cadr tree)))
+ (start (and bounds (car bounds)))
+ (end (and bounds (cdr bounds))))
+ (if (and
+ (numberp start)
+ (<= start point end)
+ (not (= (save-excursion
+ (goto-char start) (beginning-of-line) (point))
+ (save-excursion
+ (goto-char end) (beginning-of-line) (point)))))
+ (list start end)
+ nil)))
+ (t nil)))))
+
+(defun yaml-pro-get-block (tree point)
+ "Return subtree from TREE that best contain POINT."
+ (if (not (listp tree))
+ nil
+ (let ((sub-blocks
+ (seq-filter #'identity
+ (seq-map (lambda (st) (yaml-pro-get-block st point))
+ tree))))
+ (cond
+ (sub-blocks
+ ;; TODO should find best match instead of firt (?)
+ (car sub-blocks))
+ ((stringp (car tree))
+ (let* ((bounds (yaml-pro--get-yaml-position (cadr tree)))
+ (start (and bounds (car bounds)))
+ (end (and bounds (cdr bounds))))
+ (if (and (numberp start)
+ (<= start point end)
+ ;; hack to get small maps to not get selected
+ (yaml-pro--fix-bounds (list start end)))
+ (list start end)
+ nil)))
+ (t nil)))))
+
+(defun yaml-pro--fix-bounds (bounds)
+ "Adjust BOUNDS to proper fold points."
+ (seq-let (beg end) bounds
+ (save-excursion
+ (goto-char beg)
+ (cond
+ ((looking-at-p "[ \t]*-")
+ (setq beg (1- (point))))
+ ((and (not (looking-at "{"))
+ (not (looking-at "\\[")))
+ (end-of-line)
+ (setq beg (point)))
+ ((looking-at-p "\\(\\[\\|{\\)")
+ (setq beg (1+ (point))))))
+ (save-excursion
+ (goto-char end)
+ (cond
+ ((looking-back "\n" (- (point) 2))
+ (setq end (1- end)))
+ ((looking-back "\\(\\]\\|\\}\\)" (- (point) 2))
+ (setq end (1- end)))))
+ (if (>= beg end)
+ nil
+ (list beg end))))
+
+(defun yaml-pro--search-location (tree point path)
+ "Return path up to POINT of TREE having visited at PATH."
+ (cond
+ ((stringp tree)
+ (let ((pos (yaml-pro--get-yaml-position tree)))
+ (when (<= (car pos) point (cdr pos))
+ path)))
+ (t
+ (seq-find #'identity
+ (seq-map
+ (lambda (tuple)
+ (let* ((key (car tuple))
+ (key-pos (and (stringp key)
+ (yaml-pro--get-yaml-position key)))
+ (val (cdr tuple))
+ (val-pos (and (stringp val)
+ (yaml-pro--get-yaml-position val))))
+ (cond
+ ((and key-pos (<= (car key-pos) point (cdr key-pos)))
+ path)
+ ((and val-pos (<= (car val-pos) point (cdr val-pos)))
+ (cons key path))
+ ((vectorp val)
+ (catch 'found
+ (dotimes (i (length val))
+ (let* ((elt (aref val i))
+ (res (yaml-pro--search-location
+ elt
+ point
+ (cons (number-to-string i)
+ (cons key path)))))
+ (when res
+ (throw 'found res))))
+ nil))
+ ((listp val)
+ (yaml-pro--search-location val point (cons key path)))
+ (t nil))))
+ tree)))))
+
+(defun yaml-pro--path-at-point ()
+ "Return the object path to current point.
+
+NOTE: This is an experimental feature."
+ ;; first look for current position
+ (let* ((parse (yaml-parse-string-with-pos (buffer-string)))
+ (path (yaml-pro--search-location parse (point) '())))
+ (seq-map
+ (lambda (s)
+ (set-text-properties 0 (length s) nil s)
+ s)
+ (or (nreverse path)
+ ;; if not found, go back before ":" and try again
+ (save-excursion
+ (when (looking-back "[ \"a-zA-Z_-]+: +[ \"a-zA-Z_-]+" (- (point) 60))
+ (beginning-of-line)
+ (skip-chars-forward " ")
+ (let ((path (yaml-pro--search-location parse (point) '())))
+ (nreverse path))))))))
+
+(defun yaml-pro--flatten-tree (tree)
+ "Return a \"flattened\" copy of TREE. Copied from Emacs 27.1."
+ (let (elems)
+ (while (consp tree)
+ (let ((elem (pop tree)))
+ (while (consp elem)
+ (push (cdr elem) tree)
+ (setq elem (car elem)))
+ (if elem (push elem elems))))
+ (if tree (push tree elems))
+ (nreverse elems)))
+
+(defun yaml-pro--extract-paths (tree &optional path)
+ "Given TREE of parse, return all paths of tree to leaf nodes.
+
+PATH is the current path we have already traversed down."
+ (cond
+ ((listp tree)
+ (yaml-pro--flatten-tree
+ (seq-map (lambda (key+val)
+ (yaml-pro--extract-paths (cdr key+val)
+ (append path (list (car key+val)))))
+ tree)))
+ ((vectorp tree)
+ (yaml-pro--flatten-tree
+ (seq-mapn
+ (lambda (n val)
+ (yaml-pro--extract-paths val (append path (list (format "[%d]" n)))))
+ (number-sequence 0 (1- (length tree)))
+ tree)))
+ (t (concat (string-join path " ") ": " tree))))
+
+(defun yaml-pro-hide-overlay (ov)
+ "Put fold-related properties on overlay OV."
+ (overlay-put ov 'invisible 'yaml-pro)
+ (overlay-put ov 'display "...")
+ (overlay-put ov 'face 'yaml-pro-fold-replacement-face))
+
+(defun yaml-pro-show-overlay (ov)
+ "Remove fold-related properties of overlay OV."
+ (overlay-put ov 'invisible nil)
+ (overlay-put ov 'display nil)
+ (overlay-put ov 'face nil))
+
+(defun yaml-pro--get-last-yaml-pos (str)
+ "Return the last-most text-property `yaml-position' of STR."
+ (let ((i (1- (length str)))
+ (pos))
+ (while (or (not pos) (< i 0))
+ (let ((at-pos (get-text-property i 'yaml-position str)))
+ (when at-pos
+ (setq pos at-pos))
+ (cl-decf i)))
+ pos))
+
+(defun yaml-pro-jump ()
+ "Jump to a specified path."
+ (interactive)
+ (let* ((tree (yaml-parse-string-with-pos (buffer-string)))
+ (paths (yaml-pro--extract-paths tree))
+ (selected (completing-read "Jump to: " paths nil t))
+ ;; get original string which has properties intact
+ (original-str (seq-find (lambda (s) (string= s selected)) paths))
+ (pos (yaml-pro--get-last-yaml-pos original-str)))
+ (goto-char (car pos))))
+
+(declare-function consult--read "consult")
+(declare-function consult--overlay "consult")
+(declare-function consult--invisible-open-temporarily "consult")
+
+(defun yaml-pro--consult-jump-preview ()
+ "Return a consult function for displaying current selection."
+ (let ((invisible)
+ (overlays)
+ (saved-pos (point-marker))
+ (saved-min (point-min-marker))
+ (saved-max (point-max-marker)))
+ (lambda (cand restore)
+ (mapc #'funcall invisible)
+ (mapc #'delete-overlay overlays)
+ (cond
+ (restore
+ (let ((saved-buffer (marker-buffer saved-pos)))
+ (set-buffer saved-buffer)
+ (narrow-to-region saved-min saved-max)
+ (goto-char saved-pos)))
+ (cand
+ (let ((pos (and (not (string-blank-p cand))
+ (yaml-pro--get-last-yaml-pos cand))))
+ (when pos
+ (widen)
+ (goto-char (car pos))
+ (run-hooks 'consult-after-jump-hook)
+ (setq invisible (consult--invisible-open-temporarily)
+ overlays
+ (list (consult--overlay (car pos) (cdr pos)
+ 'face 'consult-preview-cursor
+ 'window (selected-window)))))))))))
+
+;;; Interactive commands
+
+(defun yaml-pro-consult-jump ()
+ "Jump to a specified path."
+ (interactive)
+ (let* ((tree (yaml-parse-string-with-pos (buffer-string)))
+ (paths (yaml-pro--extract-paths tree))
+ (sorted-paths (seq-sort-by (lambda (path)
+ (car (yaml-pro--get-last-yaml-pos path)))
+ #'< paths))
+ (selected (consult--read
+ sorted-paths
+ :prompt "Jump to: "
+ :history 'yaml-pro-jump
+ :require-match t
+ :sort nil
+ :state (let ((preview (yaml-pro--consult-jump-preview)))
+ (lambda (str restore)
+ (funcall preview str restore)))))
+ (original-str (seq-find (lambda (s) (string= s selected)) paths)))
+ (goto-char (car (yaml-pro--get-last-yaml-pos original-str)))))
+
+(defun yaml-pro-fold-at-point ()
+ "Fold YAML at point."
+ (interactive)
+ (save-excursion
+ (skip-syntax-forward " " (line-end-position))
+ (let ((parse-tree (yaml-pro--get-buffer-tree)))
+ (let* ((bounds (yaml-pro--fix-bounds
+ (yaml-pro-get-block parse-tree (point))))
+ (beg (car bounds))
+ (end (cadr bounds)))
+ (when bounds
+ ;; Delete all inner folds before creating outer one
+ (let ((ovs (overlays-in beg end)))
+ (dolist (ov ovs)
+ (when (eql (overlay-get ov 'creator) 'yaml-pro)
+ (delete-overlay ov))))
+ (let ((ov (make-overlay beg end)))
+ (overlay-put ov 'creator 'yaml-pro)
+ (overlay-put ov 'invisible 'yaml-pro)
+ (overlay-put ov 'isearch-open-invisible 'yaml-pro-isearch-show)
+ (overlay-put ov 'isearch-open-invisible-temporary
+ (lambda (ov hide-p)
+ (if hide-p (yaml-pro-hide-overlay ov)
+ (yaml-pro-show-overlay ov))))
+ (overlay-put ov 'display "...")
+ (overlay-put ov 'face 'yaml-pro-fold-replacement-face)))))))
+
+(defun yaml-pro-unfold-at-point ()
+ "Unfold YAML at point."
+ (interactive)
+ (save-excursion
+ (cond
+ ((looking-at ".*:")
+ (let ((ovs (overlays-in (point)
+ (save-excursion (end-of-line) (1+ (point))))))
+ (dolist (ov ovs)
+ (when (eql (overlay-get ov 'creator) 'yaml-pro)
+ (delete-overlay ov)))))
+ (t
+ (let ((ovs (overlays-at (1+ (point)))))
+ (dolist (ov ovs)
+ (when (eql (overlay-get ov 'creator) 'yaml-pro)
+ (delete-overlay ov))))))))
+
+(defun yaml-pro-up-level ()
+ "Move the point to the parent tree."
+ (interactive)
+ (if (and (bolp) (not (looking-at "[ \n\t#]")))
+ (goto-char (point-min))
+ (let* ((start (point))
+ (parse-tree (yaml-pro--get-buffer-tree))
+ (bounds (yaml-pro-get-block-bounds parse-tree (point))))
+ (goto-char (car bounds))
+ (when (= start (point))
+ (let* ((parse-tree (yaml-pro--get-buffer-tree))
+ (bounds (yaml-pro-get-parent-block parse-tree (point))))
+ (goto-char (car bounds))
+ (when (= start (point))
+ ;; the block we're at and it's parent have the same start,
+ ;; move back one char and try again
+ (forward-char -1)
+ (yaml-pro-up-level))))
+ (when (and (bolp) (looking-at "[ \t]"))
+ ;; Don't let the command end at a block that begins a line
+ (yaml-pro-up-level)))))
+
+(defun yaml-pro-kill-subtree ()
+ "Kill the entire subtree located at the current point."
+ (interactive)
+ (let* ((parse-tree (yaml-pro--get-buffer-tree))
+ (bounds (yaml-pro-get-block-bounds parse-tree (point)))
+ (start (car bounds))
+ (end (cadr bounds)))
+ (when (save-excursion
+ (goto-char end)
+ (looking-back "\n" (- (point) 2)))
+ (setq end (1- end)))
+ (kill-region start end)))
+
+(defun yaml-pro-prev-subtree ()
+ "Move the point to the previous sibling subtree."
+ (interactive)
+ (let* ((start-pos (point))
+ (parse-tree (yaml-pro--get-buffer-tree))
+ (at-bounds (yaml-pro-get-block-bounds parse-tree (point))))
+ (goto-char (car at-bounds))
+ (let ((at-col (current-column))
+ (at-dash-p (looking-at "-")))
+ (catch 'done
+ (while (not (bobp))
+ (forward-line -1)
+ (while (looking-at "[ \n]*$")
+ (forward-line -1))
+ (if at-dash-p
+ (skip-chars-forward " \n")
+ (skip-chars-forward " \n-"))
+ (when (and (< (current-column) at-col)
+ (not (looking-at "#")))
+ (goto-char start-pos)
+ (ding)
+ (throw 'done nil))
+ (when (and (= (current-column) at-col)
+ (not (looking-at "#")))
+ (throw 'done t)))
+ (if (looking-at "#")
+ (progn
+ (goto-char start-pos)
+ (ding)
+ nil)
+ t)))))
+
+(defun yaml-pro-next-subtree ()
+ "Move the point to the next sibling subtree."
+ (interactive)
+ (let* ((start-pos (point))
+ (start-col (current-column))
+ (parse-tree (yaml-pro--get-buffer-tree))
+ (at-bounds (yaml-pro-get-block-bounds parse-tree (point))))
+ (goto-char (cadr at-bounds))
+ (skip-chars-forward " \n")
+ (if (or (not (= start-col (current-column)))
+ (eobp))
+ (progn
+ (ding)
+ (goto-char start-pos)
+ nil)
+ t)))
+
+(defun yaml-pro-indent-subtree ()
+ "Indent the current subtree by one level.
+
+Indentation is controlled by the variable `yaml-pro-indent'."
+ (interactive)
+ (save-excursion
+ (back-to-indentation)
+ (let* ((parse-tree (yaml-pro--get-buffer-tree))
+ (at-bounds (yaml-pro-get-block-bounds parse-tree (point))))
+ (goto-char (car at-bounds))
+ (if (not (looking-back "^[ ]*" nil))
+ (progn
+ (forward-line 0)
+ (skip-chars-forward " ")
+ (yaml-pro-indent-subtree))
+ (let ((beginning-line (line-number-at-pos)))
+ (goto-char (cadr at-bounds))
+ (when (looking-back "\n" (- (point) 2))
+ (forward-char -1))
+ (forward-line 0)
+ (while (>= (line-number-at-pos) beginning-line)
+ (insert (make-string yaml-pro-indent ?\s))
+ (forward-line -1)))))))
+
+(defun yaml-pro-unindent-subtree ()
+ "Unindent the current subtree by one level.
+
+Indentation is controlled by the variable `yaml-pro-indent'."
+ (interactive)
+ (let* ((parse-tree (yaml-pro--get-buffer-tree))
+ (at-bounds (yaml-pro-get-block-bounds parse-tree (point))))
+ (save-excursion
+ (goto-char (car at-bounds))
+ ;; ensure that the subtree can be unintented
+ (save-excursion
+ (forward-line 0)
+ (unless (looking-at-p (make-string yaml-pro-indent ?\s))
+ (error "Subtree can't be unintented further")))
+ (if (not (looking-back "^[ ]*" nil))
+ (progn
+ (forward-line 0)
+ (skip-chars-forward " ")
+ (yaml-pro-indent-subtree))
+ (let ((beginning-line (line-number-at-pos)))
+ (goto-char (cadr at-bounds))
+ (when (looking-back "\n" (- (point) 2))
+ (forward-char -1))
+ (forward-line 0)
+ (while (>= (line-number-at-pos) beginning-line)
+ (when (looking-at-p (make-string yaml-pro-indent ?\s))
+ (delete-char yaml-pro-indent))
+ (forward-line -1)))))))
+
+(defun yaml-pro-move-subtree-up ()
+ "Swap the current subtree with the previous one."
+ (interactive)
+ (let* ((parse-tree (yaml-pro--get-buffer-tree))
+ (at-bounds (yaml-pro-get-block-bounds parse-tree (point)))
+ (at-contents (buffer-substring (car at-bounds) (cadr at-bounds)))
+ (prev-bounds (save-excursion
+ (let ((ok (yaml-pro-prev-subtree)))
+ (and ok (yaml-pro-get-block-bounds parse-tree
+ (point))))))
+ (prev-contents (and prev-bounds
+ (buffer-substring (car prev-bounds)
+ (cadr prev-bounds)))))
+ (when (not prev-bounds)
+ (error "Can't move subtree"))
+ (goto-char (car at-bounds))
+ (delete-region (car at-bounds) (cadr at-bounds))
+ (insert prev-contents)
+ (goto-char (car prev-bounds))
+ (delete-region (car prev-bounds) (cadr prev-bounds))
+ (insert at-contents)
+ (goto-char (car prev-bounds))))
+
+(defun yaml-pro-move-subtree-down ()
+ "Swap the current subtree with the previous one."
+ (interactive)
+ (yaml-pro-next-subtree)
+ (yaml-pro-move-subtree-up)
+ (yaml-pro-next-subtree))
+
+(defun yaml-pro-create-index ()
+ "Create an imenu index using the legacy parser."
+ (let* ((tree (yaml-parse-string-with-pos (buffer-string)))
+ (paths (yaml-pro--extract-paths tree))
+ (sorted-paths (seq-sort-by (lambda (path)
+ (car (yaml-pro--get-last-yaml-pos path)))
+ #'< paths)))
+ (seq-map
+ (lambda (item)
+ (let ((pos (car (yaml-pro--get-last-yaml-pos item))))
+ (cons item pos)))
+ sorted-paths)))
+
+(defconst yaml-pro-mode-map
+ (let ((map (make-sparse-keymap)))
+ (prog1 map
+ (define-key map (kbd "C-c C-x C-w") #'yaml-pro-kill-subtree)
+
+ (define-key map (kbd "C-c C-u") #'yaml-pro-up-level)
+
+ (define-key map (kbd "C-c C-p") #'yaml-pro-prev-subtree)
+ (define-key map (kbd "C-c C-n") #'yaml-pro-next-subtree)
+
+ (define-key map (kbd "C-c C-f") #'yaml-pro-fold-at-point)
+ (define-key map (kbd "C-c C-o") #'yaml-pro-unfold-at-point)
+
+ (define-key map (kbd "s-<up>") #'yaml-pro-move-subtree-up)
+ (define-key map (kbd "s-<down>") #'yaml-pro-move-subtree-down)
+
+ (define-key map (kbd "C-c '") #'yaml-pro-edit-scalar)
+
+ (define-key map (kbd "C-c >") #'yaml-pro-indent-subtree)
+
+ (define-key map (kbd "C-c <") #'yaml-pro-unindent-subtree)
+
+ (define-key map (kbd "C-c C-j") (if (featurep 'consult)
+ #'yaml-pro-consult-jump
+ #'yaml-pro-jump)))))
+
+(make-obsolete 'yaml-pro-consult-jump "Use imenu feature instead of this command." "0.3.2")
+
+(defconst yaml-pro-required-yaml-parser-version "0.5.1")
+
+;;;###autoload
+(define-minor-mode yaml-pro-mode
+ "Binds additional functions to aid in editing YAML files.
+
+\\{yaml-pro-mode-map}"
+ :init-value nil
+ :group 'yaml-pro
+ :keymap yaml-pro-mode-map
+ (if yaml-pro-mode
+ (progn
+ (when (or (not (boundp 'yaml-parser-version))
+ (not yaml-parser-version)
+ (version< yaml-parser-version
+ yaml-pro-required-yaml-parser-version))
+ (error "Unsupported yaml.el version. \
+Ensure that yaml.el package installed and at version %s"
+ yaml-pro-required-yaml-parser-version))
+ (setq imenu-generic-expression nil)
+ (setq imenu-create-index-function #'yaml-pro-create-index)
+ (when (equal mode-name "YAML")
+ (add-hook 'after-change-functions #'yaml-pro--after-change-hook nil t)))
+ (remove-hook 'after-change-functions #'yaml-pro--after-change-hook t)))
+
+(provide 'yaml-pro)
+
+;;; yaml-pro.el ends here