summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-03-02 19:46:38 -0600
committerCraig Jennings <c@cjennings.net>2026-03-02 19:46:38 -0600
commitb5cae72d8f824f235f351821f7d7052b66bc2513 (patch)
tree264261c26ec5a6c721e5dbd83e93237e842e53b5
parenta46f8af939b112b603a2c95b2e83a1932b208e20 (diff)
feat(json,yaml): add tree-sitter modes, formatting, and jq integration
New prog-json module: json-ts-mode with jq formatting (C-; f) and jq-interactively (C-c C-q). Upgraded prog-yaml to yaml-ts-mode with prettier formatting. Both use treesit-auto for grammar management. Includes 18 new tests (10 JSON, 8 YAML), 185/185 passing.
-rw-r--r--assets/abbrev_defs13
-rw-r--r--init.el1
-rw-r--r--modules/org-export-config.el2
-rw-r--r--modules/prog-json.el60
-rw-r--r--modules/prog-webdev.el8
-rw-r--r--modules/prog-yaml.el44
-rw-r--r--tests/test-prog-json--json-format-buffer.el109
-rw-r--r--tests/test-prog-yaml--yaml-format-buffer.el76
8 files changed, 292 insertions, 21 deletions
diff --git a/assets/abbrev_defs b/assets/abbrev_defs
index c3af4548..559a4df2 100644
--- a/assets/abbrev_defs
+++ b/assets/abbrev_defs
@@ -34,7 +34,7 @@
("aethetically" "aesthetically" nil :count 0)
("agression" "aggression" nil :count 0)
("agressive" "aggressive" nil :count 0)
- ("ahve" "have" nil :count 15)
+ ("ahve" "have" nil :count 16)
("aknowledge" "acknowledge" nil :count 0)
("alegiance" "allegiance" nil :count 0)
("allegaince" "allegiance" nil :count 0)
@@ -64,6 +64,7 @@
("autopayment" "auto-payment" nil :count 0)
("autopayments" "auto-payments" nil :count 0)
("availabilty" "availability" nil :count 0)
+ ("avialable" "available" nil :count 0)
("balconly" "balcony" nil :count 0)
("bandanas" "bandannas" nil :count 3)
("beatiful" "beautiful" nil :count 0)
@@ -183,7 +184,7 @@
("finidng" "finding" nil :count 0)
("finshed" "finished" nil :count 0)
("firey" "fiery" nil :count 0)
- ("firsst" "first" nil :count 3)
+ ("firsst" "first" nil :count 4)
("flekey" "flakey" nil :count 0)
("forevr" "forever" nil :count 0)
("foriegn" "foreign" nil :count 0)
@@ -197,6 +198,7 @@
("garanty" "guarantee" nil :count 0)
("garentee" "guarantee" nil :count 0)
("generousity" "generosity" nil :count 0)
+ ("geolocation" "geo" nil :count 0)
("goinig" "going" nil :count 0)
("grat" "great" nil :count 0)
("greatful" "grateful" nil :count 0)
@@ -298,7 +300,7 @@
("oppositiion" "opposition" nil :count 0)
("opppsite" "opposite" nil :count 0)
("orignal" "original" nil :count 0)
- ("ot" "to" nil :count 43)
+ ("ot" "to" nil :count 44)
("otehr" "other" nil :count 3)
("otes" "notes" nil :count 0)
("outgoign" "outgoing" nil :count 0)
@@ -366,6 +368,7 @@
("rythem" "rhythm" nil :count 0)
("rythm" "rhythm" nil :count 0)
("sargent" "sergeant" nil :count 0)
+ ("sattelite" "satellite" nil :count 0)
("sattelites" "satellites" nil :count 0)
("scheudle" "schedule" nil :count 4)
("secratary" "secretary" nil :count 0)
@@ -398,8 +401,8 @@
("takss" "tasks" nil :count 3)
("talekd" "talked" nil :count 0)
("talkign" "talking" nil :count 6)
- ("teh" "the" nil :count 163)
- ("tehir" "their" nil :count 6)
+ ("teh" "the" nil :count 170)
+ ("tehir" "their" nil :count 7)
("tehre" "there" nil :count 3)
("testimentary" "testamentary" nil :count 1)
("thansk" "thanks" nil :count 3)
diff --git a/init.el b/init.el
index 21e46309..1339e7e9 100644
--- a/init.el
+++ b/init.el
@@ -103,6 +103,7 @@
(require 'prog-shell) ;; combine elsewhere
(require 'prog-python)
(require 'prog-webdev)
+(require 'prog-json)
(require 'prog-yaml)
;; ---------------------------------- Org Mode ---------------------------------
diff --git a/modules/org-export-config.el b/modules/org-export-config.el
index 4451eddd..688d8f99 100644
--- a/modules/org-export-config.el
+++ b/modules/org-export-config.el
@@ -49,7 +49,7 @@
(setq org-export-with-tasks '("TODO")) ;; export with tasks by default
(setq org-export-with-tasks nil) ;; export WITHOUT tasks by default
(setq org-export-with-toc t) ;; export WITH table of contents by default
- (setq org-export-initial-scope 'subtree) ;; 'buffer is your other choice
+ (setq org-export-initial-scope 'buffer) ;; 'subtree is your other choice
(setq org-export-with-author nil)) ;; export without author by default
(use-package ox-html
diff --git a/modules/prog-json.el b/modules/prog-json.el
new file mode 100644
index 00000000..6dba6dee
--- /dev/null
+++ b/modules/prog-json.el
@@ -0,0 +1,60 @@
+;;; prog-json.el --- JSON Editing, Formatting, and jq Integration -*- lexical-binding: t; coding: utf-8; -*-
+;; Author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;; JSON editing with tree-sitter highlighting, one-key formatting, and
+;; interactive jq queries against the current buffer.
+;;
+;; Features:
+;; - Tree-sitter: Better syntax highlighting and structural navigation
+;; - Formatting: Pretty-print with sorted keys via C-; f
+;; - jq: Interactive jq REPL against current JSON buffer
+;;
+;; Workflow:
+;; 1. Open .json file → json-ts-mode with tree-sitter highlighting
+;; 2. C-; f → Format/pretty-print the buffer
+;; 3. C-c C-q → Open jq interactive buffer to query/transform JSON
+
+;;; Code:
+
+(defvar json-ts-mode-map)
+
+;; -------------------------------- JSON Mode ----------------------------------
+;; tree-sitter mode for JSON files (built-in, Emacs 29+)
+;; NOTE: No :mode directive here — treesit-auto (in prog-general.el) handles
+;; the auto-mode-alist mapping and auto-installs the grammar on first use.
+
+(use-package json-ts-mode
+ :ensure nil
+ :defer t)
+
+;; -------------------------------- Formatting ---------------------------------
+;; pretty-print with sorted keys, bound to standard format key
+
+(defun cj/json-format-buffer ()
+ "Format the current JSON buffer with sorted keys.
+Uses jq if available for reliable formatting, otherwise falls
+back to the built-in `json-pretty-print-buffer-ordered'."
+ (interactive)
+ (if (executable-find "jq")
+ (let ((point (point)))
+ (shell-command-on-region (point-min) (point-max) "jq --sort-keys ." nil t)
+ (goto-char (min point (point-max))))
+ (json-pretty-print-buffer-ordered)))
+
+(defun cj/json-setup ()
+ "Set up JSON buffer keybindings."
+ (local-set-key (kbd "C-; f") #'cj/json-format-buffer))
+
+(add-hook 'json-ts-mode-hook #'cj/json-setup)
+
+;; --------------------------------- jq Mode -----------------------------------
+;; interactive jq queries against JSON buffers
+
+(use-package jq-mode
+ :defer t
+ :bind (:map json-ts-mode-map
+ ("C-c C-q" . jq-interactively)))
+
+(provide 'prog-json)
+;;; prog-json.el ends here.
diff --git a/modules/prog-webdev.el b/modules/prog-webdev.el
index a45bd376..c0a5980b 100644
--- a/modules/prog-webdev.el
+++ b/modules/prog-webdev.el
@@ -3,7 +3,6 @@
;;
;;; Commentary:
;; Open a project file and Emacs selects the right helper:
-;; - *.json buffers drop into json-mode for quick structural edits.
;; - *.js buffers jump into js2-mode for linty feedback.
;; - Mixed HTML templates land in web-mode which chains Tide and CSS Eldoc.
;;
@@ -17,13 +16,6 @@
;;; Code:
-;; --------------------------------- JSON Mode ---------------------------------
-;; mode for editing JavaScript Object Notation (JSON) data files
-
-(use-package json-mode
- :mode ("\\.json\\'" . json-mode)
- :defer .5)
-
;; ---------------------------------- JS2 Mode ---------------------------------
;; javascript editing mode
diff --git a/modules/prog-yaml.el b/modules/prog-yaml.el
index 1a970313..8411f04c 100644
--- a/modules/prog-yaml.el
+++ b/modules/prog-yaml.el
@@ -2,17 +2,47 @@
;; author: Craig Jennings <c@cjennings.net>
;;; Commentary:
+;; YAML editing with tree-sitter highlighting and one-key formatting.
+;;
+;; Features:
+;; - Tree-sitter: Syntax highlighting and structural navigation
+;; - Formatting: Normalize indentation and style via C-; f
+;;
+;; Workflow:
+;; 1. Open .yml/.yaml file → yaml-ts-mode with tree-sitter highlighting
+;; 2. C-; f → Format buffer with prettier
;;; Code:
-(use-package yaml-mode
- :defer .5
- :commands (yaml-mode)
- :config
- (add-to-list 'auto-mode-alist '("\\.yml\\'" . yaml-mode))
- (add-to-list 'auto-mode-alist '("\\.yaml\\'" . yaml-mode)))
+;; -------------------------------- YAML Mode ----------------------------------
+;; tree-sitter mode for YAML files (built-in, Emacs 29+)
+;; NOTE: No :mode directive — treesit-auto (in prog-general.el) handles
+;; the auto-mode-alist mapping and auto-installs the grammar on first use.
-(add-hook 'yaml-mode-hook ' flycheck-mode-hook)
+(use-package yaml-ts-mode
+ :ensure nil
+ :defer t)
+
+;; -------------------------------- Formatting ---------------------------------
+;; normalize indentation and style, bound to standard format key
+
+(defun cj/yaml-format-buffer ()
+ "Format the current YAML buffer with prettier.
+Preserves point position as closely as possible."
+ (interactive)
+ (if (executable-find "prettier")
+ (let ((point (point)))
+ (shell-command-on-region (point-min) (point-max)
+ "prettier --parser yaml" nil t)
+ (goto-char (min point (point-max))))
+ (user-error "prettier not found; install with: npm install -g prettier")))
+
+(defun cj/yaml-setup ()
+ "Set up YAML buffer keybindings and linting."
+ (local-set-key (kbd "C-; f") #'cj/yaml-format-buffer)
+ (flycheck-mode 1))
+
+(add-hook 'yaml-ts-mode-hook #'cj/yaml-setup)
(provide 'prog-yaml)
;;; prog-yaml.el ends here
diff --git a/tests/test-prog-json--json-format-buffer.el b/tests/test-prog-json--json-format-buffer.el
new file mode 100644
index 00000000..227eafb9
--- /dev/null
+++ b/tests/test-prog-json--json-format-buffer.el
@@ -0,0 +1,109 @@
+;;; test-prog-json--json-format-buffer.el --- Tests for cj/json-format-buffer -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; Tests for the cj/json-format-buffer function in prog-json.el.
+;; Tests both the jq path and the built-in fallback path.
+
+;;; Code:
+
+(require 'ert)
+(require 'json)
+(require 'prog-json)
+
+;;; Normal Cases — jq path
+
+(ert-deftest test-prog-json--json-format-buffer-normal-formats-object ()
+ "Compact JSON object is pretty-printed with sorted keys."
+ (with-temp-buffer
+ (insert "{\"zebra\":1,\"alpha\":2}")
+ (cj/json-format-buffer)
+ (should (string= (string-trim (buffer-string))
+ "{\n \"alpha\": 2,\n \"zebra\": 1\n}"))))
+
+(ert-deftest test-prog-json--json-format-buffer-normal-formats-array ()
+ "Compact JSON array is pretty-printed."
+ (with-temp-buffer
+ (insert "[1,2,3]")
+ (cj/json-format-buffer)
+ (should (string= (string-trim (buffer-string))
+ "[\n 1,\n 2,\n 3\n]"))))
+
+(ert-deftest test-prog-json--json-format-buffer-normal-nested ()
+ "Nested JSON is pretty-printed with sorted keys at all levels."
+ (with-temp-buffer
+ (insert "{\"b\":{\"d\":1,\"c\":2},\"a\":3}")
+ (cj/json-format-buffer)
+ (should (string-match-p "\"a\": 3" (buffer-string)))
+ (should (string-match-p "\"c\": 2" (buffer-string)))
+ ;; "a" should appear before "b" (sorted)
+ (should (< (string-match "\"a\"" (buffer-string))
+ (string-match "\"b\"" (buffer-string))))))
+
+(ert-deftest test-prog-json--json-format-buffer-normal-already-formatted ()
+ "Already-formatted JSON is unchanged."
+ (let ((formatted "{\n \"alpha\": 1,\n \"beta\": 2\n}\n"))
+ (with-temp-buffer
+ (insert formatted)
+ (cj/json-format-buffer)
+ (should (string= (buffer-string) formatted)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-prog-json--json-format-buffer-boundary-empty-object ()
+ "Empty JSON object formats cleanly."
+ (with-temp-buffer
+ (insert "{}")
+ (cj/json-format-buffer)
+ (should (string= (string-trim (buffer-string)) "{}"))))
+
+(ert-deftest test-prog-json--json-format-buffer-boundary-empty-array ()
+ "Empty JSON array formats cleanly."
+ (with-temp-buffer
+ (insert "[]")
+ (cj/json-format-buffer)
+ (should (string= (string-trim (buffer-string)) "[]"))))
+
+(ert-deftest test-prog-json--json-format-buffer-boundary-scalar-string ()
+ "Bare JSON string scalar formats without error."
+ (with-temp-buffer
+ (insert "\"hello\"")
+ (cj/json-format-buffer)
+ (should (string= (string-trim (buffer-string)) "\"hello\""))))
+
+(ert-deftest test-prog-json--json-format-buffer-boundary-unicode ()
+ "JSON with unicode characters is preserved."
+ (with-temp-buffer
+ (insert "{\"emoji\":\"\\u2764\",\"name\":\"café\"}")
+ (cj/json-format-buffer)
+ (should (string-match-p "café" (buffer-string)))))
+
+;;; Fallback path — built-in formatter
+
+(ert-deftest test-prog-json--json-format-buffer-fallback-formats-without-jq ()
+ "Falls back to built-in formatter when jq is not found."
+ (cl-letf (((symbol-function 'executable-find) (lambda (_) nil)))
+ (with-temp-buffer
+ (insert "{\"b\":1,\"a\":2}")
+ (cj/json-format-buffer)
+ ;; Built-in formatter should pretty-print (key order may vary)
+ (should (string-match-p "\"a\"" (buffer-string)))
+ (should (string-match-p "\"b\"" (buffer-string)))
+ ;; Should be multi-line (formatted, not compact)
+ (should (> (count-lines (point-min) (point-max)) 1)))))
+
+;;; Error Cases
+
+(ert-deftest test-prog-json--json-format-buffer-error-invalid-json ()
+ "Invalid JSON produces an error, does not silently corrupt buffer."
+ (with-temp-buffer
+ (insert "{not valid json}")
+ (let ((original (buffer-string)))
+ ;; jq will fail on invalid JSON — buffer should not be emptied
+ (condition-case _err
+ (cj/json-format-buffer)
+ (error nil))
+ ;; Buffer should still have content (not wiped)
+ (should (> (length (buffer-string)) 0)))))
+
+(provide 'test-prog-json--json-format-buffer)
+;;; test-prog-json--json-format-buffer.el ends here
diff --git a/tests/test-prog-yaml--yaml-format-buffer.el b/tests/test-prog-yaml--yaml-format-buffer.el
new file mode 100644
index 00000000..4e928a2c
--- /dev/null
+++ b/tests/test-prog-yaml--yaml-format-buffer.el
@@ -0,0 +1,76 @@
+;;; test-prog-yaml--yaml-format-buffer.el --- Tests for cj/yaml-format-buffer -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; Tests for the cj/yaml-format-buffer function in prog-yaml.el.
+
+;;; Code:
+
+(require 'ert)
+(require 'prog-yaml)
+
+;;; Normal Cases
+
+(ert-deftest test-prog-yaml--yaml-format-buffer-normal-fixes-indentation ()
+ "Badly indented YAML is normalized to 2-space indent."
+ (with-temp-buffer
+ (insert "items:\n - one\n - two\n")
+ (cj/yaml-format-buffer)
+ (should (string= (buffer-string) "items:\n - one\n - two\n"))))
+
+(ert-deftest test-prog-yaml--yaml-format-buffer-normal-nested-map ()
+ "Nested map with inconsistent indentation is normalized."
+ (with-temp-buffer
+ (insert "server:\n host: localhost\n port: 8080\n")
+ (cj/yaml-format-buffer)
+ (should (string= (buffer-string) "server:\n host: localhost\n port: 8080\n"))))
+
+(ert-deftest test-prog-yaml--yaml-format-buffer-normal-already-formatted ()
+ "Already well-formatted YAML is unchanged."
+ (let ((formatted "name: test\nversion: 1.0\n"))
+ (with-temp-buffer
+ (insert formatted)
+ (cj/yaml-format-buffer)
+ (should (string= (buffer-string) formatted)))))
+
+(ert-deftest test-prog-yaml--yaml-format-buffer-normal-preserves-key-order ()
+ "Key order is preserved (not sorted)."
+ (with-temp-buffer
+ (insert "zebra: 1\nalpha: 2\n")
+ (cj/yaml-format-buffer)
+ (should (string-match-p "zebra.*\nalpha" (buffer-string)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-prog-yaml--yaml-format-buffer-boundary-empty-document ()
+ "Empty YAML document formats without error."
+ (with-temp-buffer
+ (insert "")
+ (cj/yaml-format-buffer)
+ (should (string= (string-trim (buffer-string)) ""))))
+
+(ert-deftest test-prog-yaml--yaml-format-buffer-boundary-single-key ()
+ "Single key-value pair formats cleanly."
+ (with-temp-buffer
+ (insert "key: value\n")
+ (cj/yaml-format-buffer)
+ (should (string= (buffer-string) "key: value\n"))))
+
+(ert-deftest test-prog-yaml--yaml-format-buffer-boundary-unicode ()
+ "YAML with unicode values is preserved."
+ (with-temp-buffer
+ (insert "name: café\ncity: Zürich\n")
+ (cj/yaml-format-buffer)
+ (should (string-match-p "café" (buffer-string)))
+ (should (string-match-p "Zürich" (buffer-string)))))
+
+;;; Error Cases
+
+(ert-deftest test-prog-yaml--yaml-format-buffer-error-no-prettier ()
+ "Signals user-error when prettier is not found."
+ (cl-letf (((symbol-function 'executable-find) (lambda (_) nil)))
+ (with-temp-buffer
+ (insert "key: value\n")
+ (should-error (cj/yaml-format-buffer) :type 'user-error))))
+
+(provide 'test-prog-yaml--yaml-format-buffer)
+;;; test-prog-yaml--yaml-format-buffer.el ends here