From 22bd5e20a653595b901ccfdc8780a0038755984d Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Wed, 13 Sep 2023 18:16:03 -0700 Subject: [PATCH] Allow formatting in partial documents (#55) * Handle invalid tokens near snap targets * Format between start and end (but no farther) --- jsonian-tests.el | 47 +++++++++++++++++----- jsonian.el | 102 ++++++++++++++++++++++++++++------------------- 2 files changed, 98 insertions(+), 51 deletions(-) diff --git a/jsonian-tests.el b/jsonian-tests.el index f28dc92..91108b2 100644 --- a/jsonian-tests.el +++ b/jsonian-tests.el @@ -448,6 +448,13 @@ $] (test "[ true$ |, false ]") (test "[ |tru$e , false ]") (test "[ true |$, false ]") + + ;; Test that we snap correctly when other parts of the buffer are not valid json + (test "`$|{null}`") + (test " =$ |[1, 2] =") + + + ;; Test in `jsonian-c-mode'. (w-comments "[ true /* comment$ */ |]") (w-comments "[ /*false*/ @@ -714,21 +721,34 @@ Specifically, we need to comply with what `completion-boundaries' describes." (face 'font-lock-keyword-face "{ \"fo$o\" // bar\n:null }") (face 'font-lock-string-face "[ \"\\\"f$oo\" ]"))) -(defun jsonian--format-string (s) - "Call `jsonian-format-region' S. To be used in testing." +(defun jsonian--format-string (s start end) + "Call `jsonian-format-region' S. To be used in testing. + +START and END provide the bound for the region. They may be +empty, in which case the whole buffer is formatted. + +START is the starting point for the region. END is a cons cell +where (car END) is the end of the region pre-formatting and (cdr +END) is the end of the region post-formatting." (with-temp-buffer (insert s) - (jsonian-format-region (point-min) (point-max)) + (apply #'jsonian-format-region + (if start + (list start end) + (list (point-min) (point-max)))) (buffer-string))) -(defun jsonian--test-format (input expected) - "Check that calling `jsonian-format-region' on INPUT yields EXPECTED." +(defun jsonian--test-format (input expected &optional start end) + "Check that calling `jsonian-format-region' on INPUT yields EXPECTED. +If START and END are provided, they are set as point and mark." + (should (and (not (xor start end)))) + (when start (should (consp end))) (let ((inhibit-message t)) ;; Validate that we get the expected result. - (should (string= (jsonian--format-string input) + (should (string= (jsonian--format-string input start (car-safe end)) expected)) ;; Validate that once formatted, calling format again is a no-op. - (should (string= (jsonian--format-string expected) + (should (string= (jsonian--format-string expected start (cdr-safe end)) expected)) ;; Validate that `jsonian--format-string' matches the behavior of `json-pretty-print'. ;; Because that `json-pretty-print-buffer' defaults to an indentation of 2, we set @@ -739,10 +759,12 @@ Specifically, we need to comply with what `completion-boundaries' describes." ;; different results. (when (> emacs-major-version 27) (let ((jsonian-indentation 2)) - (should (string= (jsonian--format-string input) + (should (string= (jsonian--format-string input start (car-safe end)) (with-temp-buffer (insert input) - (json-pretty-print-buffer) + (if start + (json-pretty-print start (car-safe end)) + (json-pretty-print-buffer)) (buffer-string)))))))) (ert-deftest jsonian-format-region () @@ -769,7 +791,12 @@ Specifically, we need to comply with what `completion-boundaries' describes." [] ] ] -")) +") + (jsonian--test-format + "`{\"small\": true}`" + "`{ + \"small\": true +}`" 2 '(17 . 23))) (provide 'jsonian-tests) ;;; jsonian-tests.el ends here diff --git a/jsonian.el b/jsonian.el index ab06b15..68f94f3 100644 --- a/jsonian.el +++ b/jsonian.el @@ -489,45 +489,49 @@ we instead move so that `char-after' gives the ?\" that begins (let* ((center (point)) left-end (left - ;; Find the left most valid starting token - (if-let (start (jsonian--pos-in-stringp)) - start - (when-let (start (jsonian--enclosing-comment-p (point))) - (goto-char start)) - - (jsonian--skip-chars-backward "\s\t\n") - (unless (bobp) - (pcase (char-before) - ((or ?: ?, ?\{ ?\} ?\[ ?\]) (1- (point))) - (?\" (jsonian--backward-string) - (point)) - (_ (while (not (or (bobp) - (memq (char-before) '(?: ?, ?\s ?\t ?\n ?\{ ?\} ?\[ ?\])))) - (backward-char)) - (unless (bobp) - (point))))))) - (right (cond - ;; If left=center, there is no point in trying to calculate `right', - ;; since it cannot be better then left. - ((eq left center) nil) - (left - ;; If we have a left token, we can just traverse forward from the left - ;; token to get the right token. - (goto-char left) - (when (and (jsonian--forward-token) - (>= center (setq left-end jsonian--last-token-end))) - ;; If center is within the node found by left, we take that - ;; token regardless of distance. This is necessary to ensure - ;; idenpotency for tightly packed tokens. - (point))) - (t - ;; We have no left token, so we need to parse to the right token. - (goto-char center) - (when-let (start (jsonian--enclosing-comment-p (point))) - (goto-char start)) - (jsonian--skip-chars-forward "\s\t\n") - (unless (eobp) - (point)))))) + (jsonian--is-token + ;; Find the left most valid starting token + (if-let (start (jsonian--pos-in-stringp)) + start + (when-let (start (jsonian--enclosing-comment-p (point))) + (goto-char start)) + + (jsonian--skip-chars-backward "\s\t\n") + (unless (bobp) + (pcase (char-before) + ((or ?: ?, ?\{ ?\} ?\[ ?\]) (1- (point))) + (?\" (jsonian--backward-string) + (point)) + (_ (while (not (or (bobp) + (memq (char-before) '(?: ?, ?\s ?\t ?\n ?\{ ?\} ?\[ ?\])))) + (backward-char)) + (unless (bobp) + (point)))))))) + (right + (jsonian--is-token + (cond + ;; If left=center, there is no point in trying to calculate `right', + ;; since it cannot be better then left. + ((eq left center) nil) + (left + ;; If we have a left token, we can just traverse forward from the left + ;; token to get the right token. + (goto-char left) + (when (and (jsonian--forward-token) + (>= center (setq left-end jsonian--last-token-end))) + ;; If center is within the node found by left, we take that + ;; token regardless of distance. This is necessary to ensure + ;; idenpotency for tightly packed tokens. + (point))) + (t + ;; We have no left token, so we need to parse to the right token. + (goto-char center) + (when-let (start (jsonian--enclosing-comment-p (point))) + (goto-char start)) + (jsonian--skip-chars-forward "\s\t\n") + (unless (eobp) + (point))))))) + ;; Move `point' to the nearest token start: `left' or `right'. (goto-char (or (if (and left right) @@ -554,6 +558,22 @@ we instead move so that `char-after' gives the ?\" that begins (or left right)) center)))) +(defun jsonian--is-token (point) + "Return POINT if it is the start of a token. +Otherwise nil is returned." + (when point + (condition-case nil + (save-excursion + (goto-char point) + ;; If not at a token, then `jsonian--forward-token' will `signal'. + (jsonian--forward-token) + ;; If we didn't signal, return `point'. + ;; + ;; This would be better expressed as a (:success t) case, but that was + ;; introduced in Emacs 28. + point) + (user-error nil)))) + (defun jsonian--display-path (path &optional pretty) "Convert the reconstructed JSON path PATH to a string. If PRETTY is non-nil, format for human readable." @@ -1957,13 +1977,13 @@ If MINIMIZE is non-nil, minimize the region instead of expanding it." (progress (make-progress-reporter "Formatting region..." start (* (- end start) 1.5)))) (set-marker-insertion-type next-token t) (while (and - (<= (point) end) + (< (point) end) (jsonian--forward-token t)) (progress-reporter-update progress (point)) ;; Delete the whitespace between the old token and the next token. (set-marker next-token (point)) (delete-region jsonian--last-token-end (point)) - (unless minimize + (unless (or minimize (>= (point) end)) ;; Unless we are minimizing, insert the appropriate whitespace. (cond ;; A space separates : from the next token