Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow formatting in partial documents #55

Merged
merged 2 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading