Skip to content

Commit

Permalink
Allow formatting in partial documents (#55)
Browse files Browse the repository at this point in the history
* Handle invalid tokens near snap targets

* Format between start and end (but no farther)
  • Loading branch information
iwahbe authored Sep 14, 2023
1 parent 318ce1b commit 22bd5e2
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 51 deletions.
47 changes: 37 additions & 10 deletions jsonian-tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -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*/
Expand Down Expand Up @@ -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
Expand All @@ -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 ()
Expand All @@ -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
102 changes: 61 additions & 41 deletions jsonian.el
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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."
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 22bd5e2

Please sign in to comment.