From f882b2e0cef294a222005ab26c067564934b9a01 Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Sat, 11 Mar 2023 18:18:51 +0000 Subject: [PATCH 1/8] refactor: Split apheleia formatter definitions from core --- apheleia-core.el | 1291 +++++++++++++++++++++++++++++++++++++ apheleia-formatters.el | 220 +++++++ apheleia.el | 1394 ---------------------------------------- 3 files changed, 1511 insertions(+), 1394 deletions(-) create mode 100644 apheleia-core.el create mode 100644 apheleia-formatters.el diff --git a/apheleia-core.el b/apheleia-core.el new file mode 100644 index 00000000..6a4cd2a8 --- /dev/null +++ b/apheleia-core.el @@ -0,0 +1,1291 @@ +;;; apheleia-core.el --- Apheleia core library -*- lexical-binding: t -*- + +;;; Commentary: + +;; `apheleia' core library. +;; +;; This file contains the core of `apheleia'. This includes `apheleia-mode', +;; utility functions for calling formatters based on `apheleia-formatters' +;; and hooks to reformat the current buffer while minimising the displacement +;; to `point'. + +;;; Code: + +(require 'cl-lib) +(require 'map) +(require 'subr-x) + +(require 'apheleia-formatters) + +(eval-when-compile + (require 'rx)) + +(defcustom apheleia-hide-log-buffers nil + "Non-nil means log buffers will be hidden. +Hidden buffers have names that begin with a space, and do not +appear in `switch-to-buffer' unless you type in a space +manually." + :type 'boolean) + +(defcustom apheleia-log-only-errors t + "Non-nil means Apheleia will only log when an error occurs. +Otherwise, Apheleia will log every time a formatter is run, even +if it is successful." + :type 'boolean) + +(defcustom apheleia-formatter-exited-hook nil + "Abnormal hook run after a formatter has finished running. +Must accept arbitrary keyword arguments. The following arguments +are defined at present: + +`:formatter' - The symbol for the formatter that was run. + +`:error' - Non-nil if the formatter failed, nil if it succeeded. + +`:log' - The log buffer for that formatter, or nil if there is +none (e.g., because logging is not enabled). + +This hook is run before `apheleia-after-format-hook', and may be +run multiple times if `apheleia-mode-alist' configures multiple +formatters to run in a chain, with one run per formatter." + :type 'hook) + +(defcustom apheleia-remote-algorithm 'cancel + "How `apheleia' should process remote files/buffers. +Set to `cancel' to immediately fail whenever you try to format a remote +buffer. + +Set to `remote' to make apheleia spawn the process and any other temporary +files on the same remote machine the buffer is on. Note due to restrictions +with `tramp' when this option is set `apheleia' will run any formatters +synchronously, meaning Emacs will block until formatting the buffer finishes. +For more information see: +https://www.mail-archive.com/tramp-devel@gnu.org/msg05623.html + +Set to `local' to make `apheleia' run the formatter on the current machine +and then write the formatted output back to the remote machine. Note some +features of `apheleia' (such as `file' in `apheleia-formatters') is not +compatible with this option and formatters relying on them will crash." + :type '(choice (const :tag "Run the formatter on the local machine" local) + (const :tag "Run the formatter on the remote machine" remote) + (const :tag "Disable formatting for remote buffers" cancel))) + +(defcustom apheleia-mode-lighter " Apheleia" + "Lighter for `apheleia-mode'." + :type '(choice :tag "Lighter" (const :tag "No lighter" nil) string) + :risky t + :group 'apheleia) + +(cl-defun apheleia--edit-distance-table (s1 s2) + "Align strings S1 and S2 for minimum edit distance. +Return the dynamic programming table as has table which maps cons +of integers (I1 . I2) to the edit distance between the first I1 +characters of S1 and the first I2 characters of S2." + (let ((table (make-hash-table :test #'equal))) + (dotimes (i1 (1+ (length s1))) + (puthash (cons i1 0) i1 table)) + (dotimes (i2 (1+ (length s2))) + (puthash (cons 0 i2) i2 table)) + (dotimes (i1 (length s1)) + ;; Iterate from 1 to length+1. + (cl-incf i1) + (dotimes (i2 (length s2)) + (cl-incf i2) + (let ((ins (1+ (gethash (cons i1 (1- i2)) table))) + (del (1+ (gethash (cons (1- i1) i2) table))) + (sub (gethash (cons (1- i1) (1- i2)) table))) + (unless (= (aref s1 (1- i1)) (aref s2 (1- i2))) + (cl-incf sub)) + (puthash (cons i1 i2) (min ins del sub) table)))) + table)) + +(defun apheleia--align-point (s1 s2 p1) + "Given strings S1 and S2 and index P1 in S1, return matching index P2 in S2. +If S1 and S2 are the same, then P1 and P2 will also be the same. +Otherwise, the text of S2 surrounding P2 is \"similar\" to the +text of S1 surrounding P1." + (let* ((table (apheleia--edit-distance-table s1 s2)) + (i1 (length s1)) + (i2 (length s2))) + (while (> i1 p1) + (let ((ins (1+ (gethash (cons i1 (1- i2)) table))) + (del (1+ (gethash (cons (1- i1) i2) table))) + (sub (gethash (cons (1- i1) (1- i2)) table))) + (unless (= (aref s1 (1- i1)) (aref s2 (1- i2))) + (cl-incf sub)) + (let ((cost (min ins del sub))) + (cond + ((= cost ins) + (cl-decf i2)) + ((= cost del) + (cl-decf i1)) + ((= cost sub) + (cl-decf i1) + (cl-decf i2)))))) + i2)) + +(defun apheleia--map-rcs-patch (func) + "Map over the RCS patch in the current buffer. +For each RCS patch command, FUNC is called with an alist that has +the following keys: + +- `command': either `addition' or `deletion' +- `start': line number, an integer +- `lines': number of lines to be inserted or removed +- `text': the string to be inserted, only for `addition' + +See +for documentation on the RCS patch format." + (save-excursion + (goto-char (point-min)) + (while (not (= (point) (point-max))) + (unless (looking-at "$\\|\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)") + (error "Malformed RCS patch: %S" (point))) + (forward-line) + (when-let ((command (match-string 1))) + (let ((start (string-to-number (match-string 2))) + (lines (string-to-number (match-string 3)))) + (pcase command + ("a" + (let ((text-start (point))) + (forward-line lines) + (funcall + func + `((command . addition) + (start . ,start) + (lines . ,lines) + (text . ,(buffer-substring-no-properties + text-start (point))))))) + ("d" + (funcall + func + `((command . deletion) + (start . ,start) + (lines . ,lines)))))))))) + +(defcustom apheleia-max-alignment-size 400 + "Maximum size for diff regions that will have point aligned. +Apheleia uses a dynamic programming algorithm to determine where +point should be placed within a diff region, but this algorithm +has quadratic runtime so it will lock up Emacs if it is run on a +diff region that is too large. The value of this variable serves +as a limit on the input size to the algorithm; larger diff +regions will still be applied, but Apheleia won't try to move +point correctly." + :type 'integer) + +(defun apheleia--apply-rcs-patch (content-buffer patch-buffer) + "Apply RCS patch. +CONTENT-BUFFER contains the text to be patched, and PATCH-BUFFER +contains the patch." + (let ((commands nil) + (point-list nil) + (window-line-list nil)) + (with-current-buffer content-buffer + (push (cons nil (point)) point-list) + (dolist (w (get-buffer-window-list nil nil t)) + (push (cons w (window-point w)) point-list) + (push (cons w (count-lines (window-start w) (point))) + window-line-list))) + (with-current-buffer patch-buffer + (apheleia--map-rcs-patch + (lambda (command) + (with-current-buffer content-buffer + ;; Could be optimized significantly by moving only as many + ;; lines as needed, rather than returning to the beginning + ;; of the buffer first. + (save-excursion + (goto-char (point-min)) + (forward-line (1- (alist-get 'start command))) + ;; Account for the off-by-one error in the RCS patch spec + ;; (namely, text is added *after* the line mentioned in + ;; the patch). + (when (eq (alist-get 'command command) 'addition) + (forward-line)) + (push `(marker . ,(point-marker)) command) + (push command commands) + ;; If we delete a region just before inserting new text + ;; at the same place, then it is a replacement. In this + ;; case, check if the replaced region includes the window + ;; point for any window currently displaying the content + ;; buffer. If so, figure out where that window point + ;; should be moved to, and record the information in an + ;; additional command. + ;; + ;; See . + ;; + ;; Note that the commands get pushed in reverse order + ;; because of how linked lists work. + (let ((deletion (nth 1 commands)) + (addition (nth 0 commands))) + (when (and (eq (alist-get 'command deletion) 'deletion) + (eq (alist-get 'command addition) 'addition) + ;; Again with the weird off-by-one + ;; computations. For example, if you replace + ;; lines 68 through 71 inclusive, then the + ;; deletion is for line 68 and the addition + ;; is for line 70. Blame RCS. + (= (+ (alist-get 'start deletion) + (alist-get 'lines deletion) + -1) + (alist-get 'start addition))) + (let ((text-start (alist-get 'marker deletion))) + (forward-line (alist-get 'lines deletion)) + (let ((text-end (point))) + (dolist (entry point-list) + ;; Check if the (window) point is within the + ;; replaced region. + (cl-destructuring-bind (w . p) entry + (when (and (< text-start p) + (< p text-end)) + (let* ((old-text (buffer-substring-no-properties + text-start text-end)) + (new-text (alist-get 'text addition)) + (old-relative-point (- p text-start)) + (new-relative-point + (if (> (max (length old-text) + (length new-text)) + apheleia-max-alignment-size) + old-relative-point + (apheleia--align-point + old-text new-text old-relative-point)))) + (goto-char text-start) + (push `((marker . ,(point-marker)) + (command . set-point) + (window . ,w) + (relative-point . ,new-relative-point)) + commands)))))))))))))) + (with-current-buffer content-buffer + (let ((move-to nil)) + (save-excursion + (dolist (command (nreverse commands)) + (goto-char (alist-get 'marker command)) + (pcase (alist-get 'command command) + (`addition + (insert (alist-get 'text command))) + (`deletion + (let ((text-start (point))) + (forward-line (alist-get 'lines command)) + (delete-region text-start (point)))) + (`set-point + (let ((new-point + (+ (point) (alist-get 'relative-point command)))) + (if-let ((w (alist-get 'window command))) + (set-window-point w new-point) + (setq move-to new-point))))))) + (when move-to + (goto-char move-to)))) + ;; Restore the scroll position of each window displaying the + ;; buffer. + (dolist (entry window-line-list) + (cl-destructuring-bind (w . old-window-line) entry + (let ((new-window-line + (count-lines (window-start w) (point)))) + (with-selected-window w + ;; Sometimes if the text is less than a buffer long, and + ;; we do a deletion, it might not be possible to keep the + ;; vertical position of point the same by scrolling. + ;; That's okay. We just go as far as we can. + (ignore-errors + (scroll-down (- old-window-line new-window-line))))))))) + +(defvar-local apheleia--current-process nil + "Current process that Apheleia is running, or nil. +Keeping track of this helps avoid running more than one process +at once.") + +(defvar apheleia--last-error-marker nil + "Marker for the last error message for any formatter. +This points into a log buffer.") + +(cl-defun apheleia--make-process + (&key name stdin stdout stderr command + remote noquery connection-type callback) + "Helper to run a formatter process asynchronously. +This starts a formatter process using COMMAND and then connects +STDIN, STDOUT and STDERR buffers to the processes different +streams. Once the process is finished CALLBACK will be invoked +with the exit-code of the formatter process as well as a boolean +saying whether the process was interrupted before completion. +REMOTE if supplied will be passed as the FILE-HANDLER argument to +`make-process'. + +See `make-process' for a description of the NAME and NOQUERY arguments." + (let ((proc + (make-process + :name name + :buffer stdout + :stderr stderr + :command command + :file-handler remote + :noquery noquery + :connection-type connection-type + :sentinel + (lambda (proc _event) + (unless (process-live-p proc) + (funcall + callback + (process-exit-status proc) + (process-get proc :interrupted))))))) + (set-process-sentinel (get-buffer-process stderr) #'ignore) + (when stdin + (set-process-coding-system + proc + nil + (buffer-local-value 'buffer-file-coding-system stdin)) + (process-send-string + proc + (with-current-buffer stdin + (buffer-string)))) + (process-send-eof proc) + proc)) + +(cl-defun apheleia--call-process + (&key name stdin stdout stderr command + remote noquery connection-type callback) + "Helper to synchronously run a formatter process. +This function essentially runs COMMAND synchronously passing STDIN +as standard input and saving output to the STDOUT and STDERR buffers. +Once the process is finished CALLBACK will be invoked with the exit +code (see `process-exit-status') of the process. + +This function accepts all the same arguments as `apheleia--make-process' +for simplicity, however some may not be used. This includes: NAME, +NO-QUERY, and CONNECTION-TYPE." + (ignore name noquery connection-type) + (let* ((run-on-remote (and (eq apheleia-remote-algorithm 'remote) + remote)) + (stderr-file (apheleia--make-temp-file run-on-remote "apheleia")) + (args + (append + (list + ;; argv[0] + (car command) + ;; If stdin we don't delete the STDIN buffer text with + ;; `call-process-region'. Otherwise we send no INFILE + ;; argument to `call-process'. + (not stdin) + ;; stdout buffer and stderr file. `call-process' cannot + ;; capture stderr into a separate buffer, the best we can + ;; do is save and read from a file. + `(,stdout ,stderr-file) + ;; Do not re/display stdout as output is recieved. + nil) + ;; argv[1:] + (cdr command)))) + (unwind-protect + (let ((exit-status + (cl-letf* ((message (symbol-function #'message)) + ((symbol-function #'message) + (lambda (format-string &rest args) + (unless (string-prefix-p "Renaming" (car args)) + (apply message format-string args))))) + (cond + ((and run-on-remote stdin) + ;; There's no call-process variant for this, we'll have to + ;; copy STDIN to a remote temporary file, create a subshell + ;; on the remote that runs the formatter and passes the temp + ;; file as stdin and then deletes it. + (let* ((remote-stdin + (apheleia--make-temp-file + run-on-remote "apheleia-stdin")) + ;; WARN: This assumes a POSIX compatible shell. + (shell + (or (bound-and-true-p tramp-default-remote-shell) + "sh")) + (shell-command + (concat + (mapconcat #'shell-quote-argument command " ") + " < " + (shell-quote-argument + (apheleia--strip-remote remote-stdin))))) + (unwind-protect + (progn + (with-current-buffer stdin + (apheleia--write-region-silently + nil nil remote-stdin)) + + (process-file + shell nil (nth 2 args) nil "-c" shell-command)) + (delete-file remote-stdin)))) + (stdin + (with-current-buffer stdin + (apply #'call-process-region + (point-min) (point-max) args))) + (run-on-remote + (apply #'process-file args)) + (t + (apply #'call-process args)))))) + ;; Save stderr from STDERR-FILE back into the STDERR buffer. + (with-current-buffer stderr + (insert-file-contents stderr-file)) + ;; I don't think it's possible to get here if the process + ;; was interrupted, since we were running it synchronously, + ;; so it should be ok to assume we pass nil to the callback. + (funcall callback exit-status nil) + ;; We return nil because there's no live process that can be + ;; returned. + nil) + (delete-file stderr-file)))) + +(cl-defun apheleia--execute-formatter-process + (&key command stdin remote callback ensure exit-status formatter) + "Wrapper for `make-process' that behaves a bit more nicely. +COMMAND is as in `make-process'. STDIN, if given, is a buffer +whose contents are fed to the process on stdin. CALLBACK is +invoked with one argument, the buffer containing the text from +stdout, when the process terminates (if it succeeds). ENSURE is a +callback that's invoked whether the process exited sucessfully or +not. EXIT-STATUS is a function which is called with the exit +status of the command; it should return non-nil to indicate that +the command succeeded. If EXIT-STATUS is omitted, then the +command succeeds provided that its exit status is 0. FORMATTER is +the symbol of the formatter that is being run, for diagnostic +purposes. FORMATTER is nil if the command being run does not +correspond to a formatter. REMOTE if non-nil will use the +formatter buffers file-handler, allowing the process to be +spawned on remote machines." + (when (process-live-p apheleia--current-process) + (message "Interrupting %s" apheleia--current-process) + (process-put apheleia--current-process :interrupted t) + (interrupt-process apheleia--current-process) + (accept-process-output apheleia--current-process 0.1 nil 'just-this-one) + (when (process-live-p apheleia--current-process) + (kill-process apheleia--current-process))) + (let* ((name (file-name-nondirectory (car command))) + (stdout (generate-new-buffer + (format " *apheleia-%s-stdout*" name))) + (stderr (generate-new-buffer + (format " *apheleia-%s-stderr*" name))) + (log-name (format "%s*apheleia-%s-log*" + (if apheleia-hide-log-buffers + " " + "") + name))) + (condition-case-unless-debug e + (progn + (setq apheleia--current-process + (funcall + (if remote #'apheleia--call-process #'apheleia--make-process) + :name (format "apheleia-%s" name) + :stdin stdin + :stdout stdout + :stderr stderr + :command command + :remote remote + :connection-type 'pipe + :noquery t + :callback + (lambda (proc-exit-status proc-interrupted) + (let ((exit-ok (and + (not proc-interrupted) + (funcall + (or exit-status #'zerop) + proc-exit-status)))) + ;; Append standard-error from current formatter + ;; to log buffer when + ;; `apheleia-log-only-errors' is nil or the + ;; formatter failed. Every process output is + ;; delimited by a line-feed character. + (unless (and exit-ok apheleia-log-only-errors) + (with-current-buffer (get-buffer-create log-name) + (special-mode) + (save-restriction + (widen) + (let ((inhibit-read-only t) + (orig-point (point)) + (keep-at-end (eobp)) + (stderr-string + (with-current-buffer stderr + (string-trim (buffer-string))))) + (goto-char (point-max)) + (skip-chars-backward "\n") + (delete-region (point) (point-max)) + (unless (bobp) + (insert + "\n\n\C-l\n")) + (unless exit-ok + (unless apheleia--last-error-marker + (setq apheleia--last-error-marker + (make-marker)) + (move-marker + apheleia--last-error-marker (point)))) + (insert + (current-time-string) + " :: " + (buffer-local-value 'default-directory stdout) + "\n$ " + (mapconcat #'shell-quote-argument command " ") + "\n\n" + (if (string-empty-p stderr-string) + "(no output on stderr)" + stderr-string) + "\n\n" + "Command " + (if exit-ok "succeeded" "failed") + " with exit code " + (number-to-string proc-exit-status) + ".\n") + ;; Known issue: this does not actually + ;; work; point is left at the end of + ;; the previous command output, instead + ;; of being moved to the end of the + ;; buffer for some reason. + (goto-char + (if keep-at-end + (point-max) + (min + (point-max) + orig-point))) + (goto-char (point-max)))))) + (when formatter + (run-hook-with-args + 'apheleia-formatter-exited-hook + :formatter formatter + :error (not exit-ok) + :log (get-buffer log-name))) + (unwind-protect + (if exit-ok + (when callback + (funcall callback stdout)) + (message + (concat + "Failed to run %s: exit status %s " + "(see %s %s)") + (car command) + proc-exit-status + (if (string-prefix-p " " log-name) + "hidden buffer" + "buffer") + (string-trim log-name))) + (when ensure + (funcall ensure)) + (ignore-errors + (kill-buffer stdout)) + (ignore-errors + (kill-buffer stderr)))))))) + (error + (ignore-errors + (kill-buffer stdout)) + (ignore-errors + (kill-buffer stderr)) + (message "Failed to run %s: %s" name (error-message-string e)))))) + +(defun apheleia-goto-error () + "Go to the most recently reported formatter error message." + (interactive) + (unless apheleia--last-error-marker + (user-error "No error has happened yet")) + (pop-to-buffer (marker-buffer apheleia--last-error-marker)) + (goto-char apheleia--last-error-marker)) + +(defun apheleia--write-region-silently + (start end filename &optional + append visit lockname mustbenew write-region) + "Like `write-region', but silent. +START, END, FILENAME, APPEND, VISIT, LOCKNAME, and MUSTBENEW are +as in `write-region'. WRITE-REGION is used instead of the actual +`write-region' function, if provided." + (funcall (or write-region #'write-region) + start end filename append 0 lockname mustbenew) + (when (or (eq visit t) (stringp visit)) + (setq buffer-file-name (if (eq visit t) + filename + visit)) + (set-visited-file-modtime) + (set-buffer-modified-p nil))) + +(defun apheleia--save-buffer-silently () + "Save the current buffer to its backing file, silently." + (cl-letf* ((write-region (symbol-function #'write-region)) + ((symbol-function #'write-region) + (lambda (start end filename &optional + append visit lockname mustbenew) + (apheleia--write-region-silently + start end filename append visit + lockname mustbenew write-region))) + (message (symbol-function #'message)) + ((symbol-function #'message) + (lambda (format &rest args) + (unless (equal format "Saving file %s...") + (apply message format args)))) + ;; Avoid triggering `after-set-visited-file-name-hook', + ;; which can have various undesired effects in particular + ;; major modes. Unfortunately, `write-file' triggers this + ;; hook unconditionally even if the filename was not + ;; changed, hence this hack :/ + (run-hooks (symbol-function #'run-hooks)) + ((symbol-function #'run-hooks) + (lambda (&rest args) + (unless (equal args '(after-set-visited-file-name-hook)) + (apply run-hooks args))))) + (save-buffer))) + +(defun apheleia--strip-remote (file-name) + "Return FILE-NAME with any TRAMP prefix removed. +If FILE-NAME is not remote, return it unchanged." + (if-let ((remote (file-remote-p file-name))) + (substring file-name (length remote)) + file-name)) + +(defun apheleia--make-temp-file (remote prefix &optional dir-flag suffix) + "Create a temporary file optionally on a remote machine. +This function calls `make-temp-file' or `make-nearby-temp-file' depending on +the value of REMOTE. + +See `make-temp-file' for a description of PREFIX, DIR-FLAG, and SUFFIX." + (funcall + (if remote + #'make-nearby-temp-file + #'make-temp-file) + prefix dir-flag suffix)) + +(defun apheleia--create-rcs-patch (old-buffer new-buffer remote callback) + "Generate RCS patch from text in OLD-BUFFER to text in NEW-BUFFER. +Once finished, invoke CALLBACK with a buffer containing the patch +as its sole argument. + +See `apheleia--run-formatters' for a description of REMOTE." + ;; Make sure at least one of the two buffers is saved to a file. The + ;; other one we can feed on stdin. + (let ((old-fname + (with-current-buffer old-buffer + (and (not (buffer-modified-p)) buffer-file-name))) + (new-fname + (with-current-buffer new-buffer + (and (not (buffer-modified-p)) buffer-file-name))) + ;; Place any temporary files we must delete in here. + (clear-files nil) + (run-on-remote (and (eq apheleia-remote-algorithm 'remote) + remote))) + (cl-labels ((;; Weird indentation because of differences in Emacs + ;; indentation algorithm between 27 and 28 + apheleia--make-temp-file-for-rcs-patch + (buffer &optional fname) + ;; Ensure there's a file with the contents of `buffer' on the + ;; target machine. `fname', if given, refers to an existing + ;; file that may not exist on the target machine and needs + ;; to be copied over. + (let ((fname-remote (and fname (file-remote-p fname)))) + (when (or (not fname) + (not (equal run-on-remote fname-remote))) + (setq fname + (apheleia--make-temp-file run-on-remote "apheleia")) + (push fname clear-files) + (with-current-buffer buffer + (apheleia--write-region-silently + (point-min) (point-max) fname))) + (apheleia--strip-remote fname)))) + ;; Ensure file is on target right machine, or create a copy of it. + (when old-fname + (setq old-fname + (apheleia--make-temp-file-for-rcs-patch old-buffer old-fname))) + (when new-fname + (setq new-fname + (apheleia--make-temp-file-for-rcs-patch new-buffer new-fname))) + ;; When neither files have an open file-handle, create one. + (unless (or old-fname new-fname) + (setq new-fname (apheleia--make-temp-file-for-rcs-patch new-buffer)))) + + (apheleia--execute-formatter-process + :command `("diff" "--rcs" "--strip-trailing-cr" "--" + ,(or old-fname "-") + ,(or new-fname "-")) + :stdin (if new-fname old-buffer new-buffer) + :callback callback + :remote remote + :ensure + (lambda () + (dolist (file clear-files) + (ignore-errors + (delete-file file)))) + :exit-status (lambda (status) + ;; Exit status is 0 if no changes, 1 if some + ;; changes, and 2 if error. + (memq status '(0 1)))))) + +(defun apheleia--safe-buffer-name () + "Return `buffer-name' without special file-system characters." + ;; See https://stackoverflow.com/q/1976007 for a list of supported + ;; characters on all systems. + (replace-regexp-in-string + (rx (or "/" "<" ">" ":" "\"" "\\" "|" "?" "*")) + "" + (buffer-name))) + +(defun apheleia--format-command (command remote &optional stdin-buffer) + "Format COMMAND into a shell command and list of file paths. +Returns a list with the car being the optional input file-name, the +cadr being the optional output file-name, the caddr is the buffer to +send as stdin to the formatter (when the input-fname is not used), +and the cdddr being the cmd to run. + +STDIN-BUFFER is the optional buffer to use when creating a temporary +file for the formatters standard input. REMOTE asserts whether the +buffer being formatted is on a remote machine or the local machine. +See `apheleia--run-formatters' for more details on the usage of REMOTE. + +If COMMAND uses the symbol `file' and the current buffer is modified +from what is written to disk, then return nil meaning meaning no +cmd is to be run." + (cl-block nil + (let* ((input-fname nil) + (output-fname nil) + ;; Either we're running remotely and the buffer is + ;; remote, or we're not running remotely and the + ;; buffer isn't remote. + (run-on-remote + (and (eq apheleia-remote-algorithm 'remote) + remote)) + ;; Whether the machine the process will run on matches + ;; the machine the buffer/file is currently on. Either + ;; we're running remotely and the buffer is remote or + ;; we're not running remotely and the buffer is not + ;; remote. + (remote-match (equal run-on-remote remote)) + (stdin (or stdin-buffer (current-buffer))) + (npx nil) + (command (apply #'list command))) + ;; TODO: Support arbitrary package managers, not just NPM. + (when (memq 'npx command) + (setq npx t) + (setq command (remq 'npx command))) + (when (and npx remote-match) + (when-let ((project-dir + (locate-dominating-file + default-directory "node_modules"))) + (let ((binary + (expand-file-name + (car command) + (expand-file-name + ".bin" + (expand-file-name + "node_modules" + project-dir))))) + (when (file-executable-p binary) + (setcar command binary))))) + (when (or (memq 'file command) (memq 'filepath command)) + ;; Fail when using file but not as the first formatter in this + ;; sequence. (But filepath is okay, since it indicates content + ;; is not actually being read from the named file.) + (when (memq 'file command) + (when stdin-buffer + (error "Cannot run formatter using `file' in a sequence unless \ +it's first in the sequence")) + (unless remote-match + (error "Formatter uses `file' but process will run on different \ +machine from the machine file is available on")) + (setq stdin nil) + ;; If `buffer-file-name' is nil then there is no backing + ;; file, so `buffer-modified-p' should be ignored (it always + ;; returns non-nil). + (when (and (buffer-modified-p) buffer-file-name) + (cl-return))) + ;; We always strip out the remote-path prefix for file/filepath. + (let ((file-name (apheleia--strip-remote + (or buffer-file-name + (concat default-directory + (apheleia--safe-buffer-name)))))) + (setq command (mapcar (lambda (arg) + (if (memq arg '(file filepath)) + file-name + arg)) + command)))) + (when (or (memq 'input command) (memq 'inplace command)) + (setq input-fname (apheleia--make-temp-file + run-on-remote "apheleia" nil + (when-let ((file-name + (or buffer-file-name + (apheleia--safe-buffer-name)))) + (file-name-extension file-name 'period)))) + (with-current-buffer stdin + (apheleia--write-region-silently nil nil input-fname)) + (let ((input-fname (apheleia--strip-remote input-fname))) + (setq command (mapcar (lambda (arg) + (if (memq arg '(input inplace)) + (progn + (setq output-fname input-fname) + input-fname) + arg)) + command)))) + (when (memq 'output command) + (setq output-fname (apheleia--make-temp-file run-on-remote "apheleia")) + (let ((output-fname (apheleia--strip-remote output-fname))) + (setq command (mapcar (lambda (arg) + (if (eq arg 'output) + output-fname + arg)) + command)))) + ;; Evaluate each element of arg that isn't a string and replace + ;; it with the evaluated value. The result of an evaluation should + ;; be a string or a list of strings. If the former its replaced as + ;; is. If the latter the contents of the list is substituted in + ;; place. + (setq command + (cl-loop + for arg in command + with val = nil + do (setq val (if (stringp arg) + arg + (eval arg))) + if val + if (and (consp val) + (cl-every #'stringp val)) + append val + else if (stringp val) + collect val + else do (error "Result of command evaluation must be a string \ +or list of strings: %S" arg))) + `(,input-fname ,output-fname ,stdin ,@command)))) + +(defun apheleia--run-formatter-process + (command buffer remote callback stdin formatter) + "Run a formatter using a shell command. +COMMAND should be a list of string or symbols for the formatter that +will format the current buffer. See `apheleia--run-formatters' for a +description of COMMAND, BUFFER, CALLBACK, REMOTE, and STDIN. FORMATTER +is the symbol of the current formatter being run, for diagnostic +purposes." + ;; NOTE: We switch to the original buffer both to format the command + ;; correctly and also to ensure any buffer local variables correctly + ;; resolve for the whole formatting process (for example + ;; `apheleia--current-process'). + (with-current-buffer buffer + (when-let ((ret (apheleia--format-command command remote stdin)) + (exec-path + (append `(,(expand-file-name + "scripts/formatters" + (file-name-directory + (file-truename + ;; Borrowed with love from Magit + (let ((load-suffixes '(".el"))) + (locate-library "apheleia")))))) + exec-path))) + (cl-destructuring-bind (input-fname output-fname stdin &rest command) ret + (when (executable-find (car command)) + (apheleia--execute-formatter-process + :command command + :stdin (unless input-fname + stdin) + :callback + (lambda (stdout) + (when output-fname + ;; Load output-fname contents into the stdout buffer. + (with-current-buffer stdout + (erase-buffer) + (insert-file-contents-literally output-fname))) + (funcall callback stdout)) + :ensure + (lambda () + (ignore-errors + (when input-fname + (delete-file input-fname)) + (when output-fname + (delete-file output-fname)))) + :remote remote + :formatter formatter)))))) + +(defun apheleia--run-formatter-function + (func buffer remote callback stdin formatter) + "Run a formatter using a Lisp function FUNC. +See `apheleia--run-formatters' for a description of BUFFER, REMOTE, +CALLBACK and STDIN. FORMATTER is the symbol of the current formatter +being run, for diagnostic purposes." + (let* ((formatter-name (if (symbolp func) (symbol-name func) "lambda")) + (scratch (generate-new-buffer + (format " *apheleia-%s-scratch*" formatter-name)))) + (with-current-buffer scratch + ;; We expect FUNC to modify scratch in place so we can't simply pass + ;; STDIN to it. When STDIN isn't nil, it's the output of a previous + ;; formatter and we want to keep it alive so we can debug any issues + ;; with it. + (insert-buffer-substring (or stdin buffer)) + (funcall func + ;; Original buffer being formatted. + :buffer buffer + ;; Buffer the formatter should modify. + :scratch scratch + ;; Name of the current formatter symbol. + :formatter formatter + ;; Callback after succesfully formatting. + :callback + (lambda () + (unwind-protect + (funcall callback scratch) + (kill-buffer scratch))) + ;; The remote part of the buffers file-name or directory. + :remote remote + ;; Whether the formatter should be run async or not. + :async (not remote) + ;; Callback when formatting scratch has failed. + :callback + (apply-partially #'kill-buffer scratch))))) + +(cl-defun apheleia-indent-lisp-buffer + (&key buffer scratch callback &allow-other-keys) + "Format a Lisp BUFFER. +Use SCRATCH as a temporary buffer and CALLBACK to apply the +transformation. + +For more implementation detail, see +`apheleia--run-formatter-function'." + (with-current-buffer scratch + (setq-local indent-line-function + (buffer-local-value 'indent-line-function buffer)) + (setq-local lisp-indent-function + (buffer-local-value 'lisp-indent-function buffer)) + (funcall (with-current-buffer buffer major-mode)) + (goto-char (point-min)) + (let ((inhibit-message t) + (message-log-max nil)) + (indent-region (point-min) (point-max))) + (funcall callback))) + +(defun apheleia--run-formatters + (formatters buffer remote callback &optional stdin) + "Run one or more code formatters on the current buffer. +FORMATTERS is a list of symbols that appear as keys in +`apheleia-formatters'. BUFFER is the `current-buffer' when this +function was first called. Once all the formatters in COMMANDS +finish succesfully then invoke CALLBACK with one argument, a +buffer containing the output of all the formatters. REMOTE asserts +whether the buffer being formatted is on a remote machine or the +current machine. It should be the output of `file-remote-p' on the +current variable `buffer-file-name'. REMOTE is the remote part of the +original buffers file-name or directory'. It's used alongside +`apheleia-remote-algorithm' to determine where the formatter process +and any temporary files it may need should be placed. + +STDIN is a buffer containing the standard input for the first +formatter in COMMANDS. This should not be supplied by the caller +and instead is supplied by this command when invoked recursively. +The stdout of the previous formatter becomes the stdin of the +next formatter." + (let ((command (alist-get (car formatters) apheleia-formatters))) + (funcall + (cond + ((consp command) + #'apheleia--run-formatter-process) + ((or (functionp command) + (symbolp command)) + #'apheleia--run-formatter-function) + (t + (error "Formatter must be a shell command or a Lisp \ +function: %s" command))) + command + buffer + remote + (lambda (stdout) + (unless (string-empty-p (with-current-buffer stdout (buffer-string))) + (if (cdr formatters) + ;; Forward current stdout to remaining formatters, passing along + ;; the current callback and using the current formatters output + ;; as stdin. + (apheleia--run-formatters + (cdr formatters) buffer remote callback stdout) + (funcall callback stdout)))) + stdin + (car formatters)))) + +(defcustom apheleia-mode-alist + '(;; php-mode has to come before cc-mode + (php-mode . phpcs) + ;; json-mode has to come before javascript-mode (aka js-mode) + (json-mode . prettier-json) + (json-ts-mode . prettier-json) + ;; rest are alphabetical + (bash-ts-mode . shfmt) + (beancount-mode . bean-format) + (c++-ts-mode . clang-format) + (caddyfile-mode . caddyfmt) + (cc-mode . clang-format) + (c-mode . clang-format) + (c-ts-mode . clang-format) + (c++-mode . clang-format) + (caml-mode . ocamlformat) + (common-lisp-mode . lisp-indent) + (crystal-mode . crystal-tool-format) + (css-mode . prettier-css) + (css-ts-mode . prettier-css) + (dart-mode . dart-format) + (elixir-mode . mix-format) + (elixir-ts-mode . mix-format) + (elm-mode . elm-format) + (fish-mode . fish-indent) + (go-mode . gofmt) + (go-mod-ts-mode . gofmt) + (go-ts-mode . gofmt) + (graphql-mode . prettier-graphql) + (haskell-mode . brittany) + (html-mode . prettier-html) + (java-mode . google-java-format) + (java-ts-mode . google-java-format) + (js3-mode . prettier-javascript) + (js-mode . prettier-javascript) + (js-ts-mode . prettier-javascript) + (kotlin-mode . ktlint) + (latex-mode . latexindent) + (LaTeX-mode . latexindent) + (lua-mode . stylua) + (lisp-mode . lisp-indent) + (nix-mode . nixfmt) + (python-mode . black) + (python-ts-mode . black) + (ruby-mode . prettier-ruby) + (ruby-ts-mode . prettier-ruby) + (rustic-mode . rustfmt) + (rust-mode . rustfmt) + (rust-ts-mode . rustfmt) + (scss-mode . prettier-scss) + (terraform-mode . terraform) + (TeX-latex-mode . latexindent) + (TeX-mode . latexindent) + (tsx-ts-mode . prettier-typescript) + (tuareg-mode . ocamlformat) + (typescript-mode . prettier-typescript) + (typescript-ts-mode . prettier-typescript) + (web-mode . prettier) + (yaml-mode . prettier-yaml) + (yaml-ts-mode . prettier-yaml)) + "Alist mapping major mode names to formatters to use in those modes. +This determines what formatter to use in buffers without a +setting for `apheleia-formatter'. The keys are major mode +symbols (matched against `major-mode' with `derived-mode-p') or +strings (matched against value of variable `buffer-file-name' +with `string-match-p'), and the values are symbols with entries +in `apheleia-formatters' (or equivalently, they are allowed +values for `apheleia-formatter'). Values can be a list of such +symnols causing each formatter in the list to be called one after +the other (with the output of the previous formatter). +Earlier entries in this variable take precedence over later ones. + +Be careful when writing regexps to include \"\\'\" and to escape +\"\\.\" in order to properly match a file extension. For example, +to match \".jsx\" files you might use \"\\.jsx\\'\". + +If a given mode derives from another mode (e.g. `php-mode' and +`cc-mode'), then ensure that the deriving mode comes before the mode +to derive from, as the list is interpreted sequentially." + :type '(alist + :key-type + (choice (symbol :tag "Major mode") + (string :tag "Buffer name regexp")) + :value-type + (choice (symbol :tag "Formatter") + (repeat + (symbol :tag "Formatter"))))) + +(defvar-local apheleia-formatter nil + "Name of formatter to use in current buffer, a symbol or nil. +If non-nil, then `apheleia-formatters' should have a matching +entry. This overrides `apheleia-mode-alist'.") +(put 'apheleia-formatter 'safe-local-variable 'symbolp) + +(defun apheleia--ensure-list (arg) + "Ensure ARG is a list of length at least 1. +When ARG is not a list its turned into a list." + (if (listp arg) + arg + (list arg))) + +(defun apheleia--get-formatters (&optional interactive) + "Return the list of formatters to use for the current buffer. +This is a list of symbols that may appear as cars in +`apheleia-formatters', or nil if no formatter is configured for +the current buffer. + +Consult the values of `apheleia-mode-alist' and +`apheleia-formatter' to determine which formatter is configured. + +If INTERACTIVE is non-nil, then prompt the user for which +formatter to run if none is configured, instead of returning nil. +If INTERACTIVE is the special symbol `prompt', then prompt +even if a formatter is configured." + (or (and (not (eq interactive 'prompt)) + (apheleia--ensure-list + (or apheleia-formatter + (cl-dolist (entry apheleia-mode-alist) + (when (or (and (symbolp (car entry)) + (derived-mode-p (car entry))) + (and (stringp (car entry)) + buffer-file-name + (string-match-p + (car entry) buffer-file-name))) + (cl-return (cdr entry))))))) + (and interactive + (list + (intern + (completing-read + "Formatter: " + (or (map-keys apheleia-formatters) + (user-error + "No formatters in `apheleia-formatters'")) + nil 'require-match)))))) + +(defun apheleia--buffer-hash () + "Compute hash of current buffer." + (if (fboundp 'buffer-hash) + (buffer-hash) + (md5 (current-buffer)))) + +(defvar apheleia--buffer-hash nil + "Return value of `buffer-hash' when formatter started running.") + +(defun apheleia--disallowed-p () + "Return an error message if Apheleia cannot be run, else nil." + (when (and buffer-file-name + (file-remote-p (or buffer-file-name + default-directory)) + (eq apheleia-remote-algorithm 'cancel)) + "Apheleia refused to run formatter due to `apheleia-remote-algorithm'")) + +;;;###autoload +(defun apheleia-format-buffer (formatter &optional callback) + "Run code formatter asynchronously on current buffer, preserving point. + +FORMATTER is a symbol appearing as a key in +`apheleia-formatters', or a list of them to run multiple +formatters in a chain. If called interactively, run the currently +configured formatters (see `apheleia-formatter' and +`apheleia-mode-alist'), or prompt from `apheleia-formatters' if +there is none configured for the current buffer. With a prefix +argument, prompt always. + +After the formatters finish running, the diff utility is invoked to +determine what changes it made. That diff is then used to apply the +formatter's changes to the current buffer without moving point or +changing the scroll position in any window displaying the buffer. If +the buffer has been modified since the formatter started running, +however, the operation is aborted. + +If the formatter actually finishes running and the buffer is +successfully updated (even if the formatter has not made any +changes), CALLBACK, if provided, is invoked with no arguments." + (interactive (progn + (when-let ((err (apheleia--disallowed-p))) + (user-error err)) + (list (apheleia--get-formatters + (if current-prefix-arg + 'prompt + 'interactive))))) + (let ((formatters (apheleia--ensure-list formatter))) + ;; Check for this error ahead of time so we don't have to deal + ;; with it anywhere in the internal machinery of Apheleia. + (dolist (formatter formatters) + (unless (alist-get formatter apheleia-formatters) + (user-error + "No such formatter defined in `apheleia-formatters': %S" + formatter))) + ;; Fail silently if disallowed, since we don't want to throw an + ;; error on `post-command-hook'. We already took care of throwing + ;; `user-error' on interactive usage above. + (unless (apheleia--disallowed-p) + (setq-local apheleia--buffer-hash (apheleia--buffer-hash)) + (let ((cur-buffer (current-buffer)) + (remote (file-remote-p (or buffer-file-name + default-directory)))) + (apheleia--run-formatters + formatters + cur-buffer + remote + (lambda (formatted-buffer) + (when (buffer-live-p cur-buffer) + (with-current-buffer cur-buffer + ;; Short-circuit. + (when + (equal + apheleia--buffer-hash (apheleia--buffer-hash)) + (apheleia--create-rcs-patch + cur-buffer formatted-buffer remote + (lambda (patch-buffer) + (when (buffer-live-p cur-buffer) + (with-current-buffer cur-buffer + (when + (equal + apheleia--buffer-hash (apheleia--buffer-hash)) + (apheleia--apply-rcs-patch + (current-buffer) patch-buffer) + (when callback + (funcall callback)))))))))))))))) + +(defcustom apheleia-post-format-hook nil + "Normal hook run after Apheleia formats a buffer successfully." + :type 'hook) + +(defcustom apheleia-inhibit-functions nil + "List of functions that prevent Apheleia from turning on automatically. +If one of these returns non-nil then `apheleia-mode' is not +enabled in a buffer, even if `apheleia-global-mode' is on. You +can still manually enable `apheleia-mode' in such a buffer. + +See also `apheleia-inhibit' for another way to accomplish a +similar task." + :type '(repeat function)) + +;; Handle recursive references. +(defvar apheleia-mode) + +;; Prevent infinite loop. +(defvar apheleia--format-after-save-in-progress nil + "Prevent `apheleia--format-after-save' from being called recursively. +This will be locally bound to t while `apheleia--format-after-save' is +operating, to prevent an infinite loop.") + +;; Autoload because the user may enable `apheleia-mode' without +;; loading Apheleia; thus this function may be invoked as an autoload. +;;;###autoload +(defun apheleia--format-after-save () + "Run code formatter for current buffer if any configured, then save." + (unless apheleia--format-after-save-in-progress + (when (and apheleia-mode (not (buffer-narrowed-p))) + (when-let ((formatters (apheleia--get-formatters))) + (apheleia-format-buffer + formatters + (lambda () + (with-demoted-errors "Apheleia: %s" + (when buffer-file-name + (let ((apheleia--format-after-save-in-progress t)) + (apheleia--save-buffer-silently))) + (run-hooks 'apheleia-post-format-hook)))))))) + +;; Use `progn' to force the entire minor mode definition to be copied +;; into the autoloads file, so that the minor mode can be enabled +;; without pulling in all of Apheleia during init. +;;;###autoload +(progn + + (define-minor-mode apheleia-mode + "Minor mode for reformatting code on save without moving point. +It is customized by means of the variables `apheleia-mode-alist' +and `apheleia-formatters'." + :lighter apheleia-mode-lighter + (if apheleia-mode + (add-hook 'after-save-hook #'apheleia--format-after-save nil 'local) + (remove-hook 'after-save-hook #'apheleia--format-after-save 'local))) + + + (defvar-local apheleia-inhibit nil + "Do not enable `apheleia-mode' automatically if non-nil. +This is designed for use in .dir-locals.el. + +See also `apheleia-inhibit-functions'.") + (put 'apheleia-inhibit 'safe-local-variable #'booleanp) + + (defun apheleia-mode-maybe () + "Enable `apheleia-mode' if allowed by user configuration. +This checks `apheleia-inhibit-functions' and `apheleia-inhibit' +to see if it is allowed." + (unless (or + apheleia-inhibit + (run-hook-with-args-until-success + 'apheleia-inhibit-functions)) + (apheleia-mode))) + + (define-globalized-minor-mode apheleia-global-mode + apheleia-mode apheleia-mode-maybe) + + (put 'apheleia-mode 'safe-local-variable #'booleanp)) + +(provide 'apheleia-core) + +;;; apheleia-core.el ends here diff --git a/apheleia-formatters.el b/apheleia-formatters.el new file mode 100644 index 00000000..cf18abdd --- /dev/null +++ b/apheleia-formatters.el @@ -0,0 +1,220 @@ +;;; apheleia-formatters.el --- Apheleia formatter definitions -*- lexical-binding: t -*- + +;;; Commentary: + +;; Formatter definitions for `apheleia'. + +;;; Code: + +(defcustom apheleia-formatters + '((bean-format . ("bean-format")) + (black . ("black" "-")) + (brittany . ("brittany")) + (caddyfmt . ("caddy" "fmt" "-")) + (clang-format . ("clang-format" + "-assume-filename" + (or (buffer-file-name) + (cdr (assoc major-mode + '((c-mode . ".c") + (c++-mode . ".cpp") + (cuda-mode . ".cu") + (protobuf-mode . ".proto")))) + ".c"))) + (crystal-tool-format . ("crystal" "tool" "format" "-")) + (dart-format . ("dart" "format")) + (elm-format . ("elm-format" "--yes" "--stdin")) + (fish-indent . ("fish_indent")) + (gofmt . ("gofmt")) + (gofumpt . ("gofumpt")) + (goimports . ("goimports")) + (google-java-format . ("google-java-format" "-")) + (isort . ("isort" "-")) + (lisp-indent . apheleia-indent-lisp-buffer) + (ktlint . ("ktlint" "--stdin" "-F")) + (latexindent . ("latexindent" "--logfile=/dev/null")) + (mix-format . ("mix" "format" "-")) + (nixfmt . ("nixfmt")) + (ocamlformat . ("ocamlformat" "-" "--name" filepath + "--enable-outside-detected-project")) + (phpcs . ("apheleia-phpcs")) + (prettier . (npx "prettier" "--stdin-filepath" filepath)) + (prettier-css + . (npx "prettier" "--stdin-filepath" filepath "--parser=css")) + (prettier-html + . (npx "prettier" "--stdin-filepath" filepath "--parser=html")) + (prettier-graphql + . (npx "prettier" "--stdin-filepath" filepath "--parser=graphql")) + (prettier-javascript + . (npx "prettier" "--stdin-filepath" filepath "--parser=babel-flow")) + (prettier-json + . (npx "prettier" "--stdin-filepath" filepath "--parser=json")) + (prettier-markdown + . (npx "prettier" "--stdin-filepath" filepath "--parser=markdown")) + (prettier-ruby + . (npx "prettier" "--stdin-filepath" filepath "--parser=ruby")) + (prettier-scss + . (npx "prettier" "--stdin-filepath" filepath "--parser=scss")) + (prettier-typescript + . (npx "prettier" "--stdin-filepath" filepath "--parser=typescript")) + (prettier-yaml + . (npx "prettier" "--stdin-filepath" filepath "--parser=yaml")) + (shfmt . ("shfmt" "-i" "4")) + (stylua . ("stylua" "-")) + (rustfmt . ("rustfmt" "--quiet" "--emit" "stdout")) + (terraform . ("terraform" "fmt" "-"))) + "Alist of code formatting commands. +The keys may be any symbols you want, and the values are shell +commands, lists of strings and symbols, or a function symbol. + +If the value is a function, the function will be called with +keyword arguments (see the implementation of +`apheleia--run-formatter-function' to see which). It should use +`cl-defun' with `&allow-other-keys' for forward compatibility. + +Otherwise in Lisp code, the format of commands is similar to what +you pass to `make-process', except as follows. + +Normally, the contents of the current buffer are passed to the +command on stdin, and the output is read from stdout. However, if +you use the symbol `file' as one of the elements of commands, +then the filename of the current buffer is substituted for +it. (Use `filepath' instead of `file' if you need the filename of +the current buffer, but you still want its contents to be passed +on stdin.) + +If you instead use the symbol `input' as one of the elements of +commands, then the contents of the current buffer are written to +a temporary file and its name is substituted for `input'. Also, +if you use the symbol `output' as one of the elements of +commands, then it is substituted with the name of a temporary +file. In that case, it is expected that the command writes to +that file, and the file is then read into an Emacs buffer. + +If you use the symbol `inplace' as one of the elements of the +list, then the contents of the current buffer are written to a +temporary file and its name is substituted for `inplace'. +However, unlike `input', it is expected that the formatter write +the formatted file back to the same file in place. In other +words, `inplace' is like `input' and `output' together. + +If you use the symbol `npx' as one of the elements of commands, +then the first string element of the command list is resolved +inside node_modules/.bin if such a directory exists anywhere +above the current `default-directory'. + +Any list elements that are not strings and not any of the special +symbols mentioned above will be evaluated when the formatter is +invoked, and spliced into the list. A form can evaluate either to +a string or to a list of strings. + +The \"scripts/formatters\" subdirectory of the Apheleia source +repository is automatically prepended to $PATH (variable +`exec-path', to be specific) when invoking external formatters. +This is intended for internal use. If you would like to define +your own script, you can simply place it on your normal $PATH +rather than using this system." + :type '(alist + :key-type symbol + :value-type + (choice + (repeat + (choice + (string :tag "Argument") + (const :tag "Look for command in node_modules/.bin" npx) + (const :tag "Name of file being formatted" filepath) + (const :tag "Name of real file used for input" file) + (const :tag "Name of temporary file used for input" input) + (const :tag "Name of temporary file used for output" output))) + (function :tag "Formatter function")))) + +(defcustom apheleia-mode-alist + '(;; php-mode has to come before cc-mode + (php-mode . phpcs) + ;; json-mode has to come before javascript-mode (aka js-mode) + (json-mode . prettier-json) + (json-ts-mode . prettier-json) + ;; rest are alphabetical + (bash-ts-mode . shfmt) + (beancount-mode . bean-format) + (c++-ts-mode . clang-format) + (caddyfile-mode . caddyfmt) + (cc-mode . clang-format) + (c-mode . clang-format) + (c-ts-mode . clang-format) + (c++-mode . clang-format) + (caml-mode . ocamlformat) + (common-lisp-mode . lisp-indent) + (crystal-mode . crystal-tool-format) + (css-mode . prettier-css) + (css-ts-mode . prettier-css) + (dart-mode . dart-format) + (elixir-mode . mix-format) + (elixir-ts-mode . mix-format) + (elm-mode . elm-format) + (fish-mode . fish-indent) + (go-mode . gofmt) + (go-mod-ts-mode . gofmt) + (go-ts-mode . gofmt) + (graphql-mode . prettier-graphql) + (haskell-mode . brittany) + (html-mode . prettier-html) + (java-mode . google-java-format) + (java-ts-mode . google-java-format) + (js3-mode . prettier-javascript) + (js-mode . prettier-javascript) + (js-ts-mode . prettier-javascript) + (kotlin-mode . ktlint) + (latex-mode . latexindent) + (LaTeX-mode . latexindent) + (lua-mode . stylua) + (lisp-mode . lisp-indent) + (nix-mode . nixfmt) + (python-mode . black) + (python-ts-mode . black) + (ruby-mode . prettier-ruby) + (ruby-ts-mode . prettier-ruby) + (rustic-mode . rustfmt) + (rust-mode . rustfmt) + (rust-ts-mode . rustfmt) + (scss-mode . prettier-scss) + (terraform-mode . terraform) + (TeX-latex-mode . latexindent) + (TeX-mode . latexindent) + (tsx-ts-mode . prettier-typescript) + (tuareg-mode . ocamlformat) + (typescript-mode . prettier-typescript) + (typescript-ts-mode . prettier-typescript) + (web-mode . prettier) + (yaml-mode . prettier-yaml) + (yaml-ts-mode . prettier-yaml)) + "Alist mapping major mode names to formatters to use in those modes. +This determines what formatter to use in buffers without a +setting for `apheleia-formatter'. The keys are major mode +symbols (matched against `major-mode' with `derived-mode-p') or +strings (matched against value of variable `buffer-file-name' +with `string-match-p'), and the values are symbols with entries +in `apheleia-formatters' (or equivalently, they are allowed +values for `apheleia-formatter'). Values can be a list of such +symnols causing each formatter in the list to be called one after +the other (with the output of the previous formatter). +Earlier entries in this variable take precedence over later ones. + +Be careful when writing regexps to include \"\\'\" and to escape +\"\\.\" in order to properly match a file extension. For example, +to match \".jsx\" files you might use \"\\.jsx\\'\". + +If a given mode derives from another mode (e.g. `php-mode' and +`cc-mode'), then ensure that the deriving mode comes before the mode +to derive from, as the list is interpreted sequentially." + :type '(alist + :key-type + (choice (symbol :tag "Major mode") + (string :tag "Buffer name regexp")) + :value-type + (choice (symbol :tag "Formatter") + (repeat + (symbol :tag "Formatter"))))) + +(provide 'apheleia-formatters) + +;;; apheleia-formatters.el ends here diff --git a/apheleia.el b/apheleia.el index 0e5b5c06..359e50b9 100644 --- a/apheleia.el +++ b/apheleia.el @@ -23,1406 +23,12 @@ ;;; Code: -(require 'cl-lib) -(require 'map) -(require 'subr-x) - -(eval-when-compile - (require 'rx)) - (defgroup apheleia nil "Reformat buffer without moving point." :group 'external :link '(url-link :tag "GitHub" "https://github.com/raxod502/apheleia") :link '(emacs-commentary-link :tag "Commentary" "apheleia")) -(defcustom apheleia-hide-log-buffers nil - "Non-nil means log buffers will be hidden. -Hidden buffers have names that begin with a space, and do not -appear in `switch-to-buffer' unless you type in a space -manually." - :type 'boolean) - -(defcustom apheleia-log-only-errors t - "Non-nil means Apheleia will only log when an error occurs. -Otherwise, Apheleia will log every time a formatter is run, even -if it is successful." - :type 'boolean) - -(defcustom apheleia-formatter-exited-hook nil - "Abnormal hook run after a formatter has finished running. -Must accept arbitrary keyword arguments. The following arguments -are defined at present: - -`:formatter' - The symbol for the formatter that was run. - -`:error' - Non-nil if the formatter failed, nil if it succeeded. - -`:log' - The log buffer for that formatter, or nil if there is -none (e.g., because logging is not enabled). - -This hook is run before `apheleia-after-format-hook', and may be -run multiple times if `apheleia-mode-alist' configures multiple -formatters to run in a chain, with one run per formatter." - :type 'hook) - -(defcustom apheleia-remote-algorithm 'cancel - "How `apheleia' should process remote files/buffers. -Set to `cancel' to immediately fail whenever you try to format a remote -buffer. - -Set to `remote' to make apheleia spawn the process and any other temporary -files on the same remote machine the buffer is on. Note due to restrictions -with `tramp' when this option is set `apheleia' will run any formatters -synchronously, meaning Emacs will block until formatting the buffer finishes. -For more information see: -https://www.mail-archive.com/tramp-devel@gnu.org/msg05623.html - -Set to `local' to make `apheleia' run the formatter on the current machine -and then write the formatted output back to the remote machine. Note some -features of `apheleia' (such as `file' in `apheleia-formatters') is not -compatible with this option and formatters relying on them will crash." - :type '(choice (const :tag "Run the formatter on the local machine" local) - (const :tag "Run the formatter on the remote machine" remote) - (const :tag "Disable formatting for remote buffers" cancel))) - -(defcustom apheleia-mode-lighter " Apheleia" - "Lighter for `apheleia-mode'." - :type '(choice :tag "Lighter" (const :tag "No lighter" nil) string) - :risky t - :group 'apheleia) - -(cl-defun apheleia--edit-distance-table (s1 s2) - "Align strings S1 and S2 for minimum edit distance. -Return the dynamic programming table as has table which maps cons -of integers (I1 . I2) to the edit distance between the first I1 -characters of S1 and the first I2 characters of S2." - (let ((table (make-hash-table :test #'equal))) - (dotimes (i1 (1+ (length s1))) - (puthash (cons i1 0) i1 table)) - (dotimes (i2 (1+ (length s2))) - (puthash (cons 0 i2) i2 table)) - (dotimes (i1 (length s1)) - ;; Iterate from 1 to length+1. - (cl-incf i1) - (dotimes (i2 (length s2)) - (cl-incf i2) - (let ((ins (1+ (gethash (cons i1 (1- i2)) table))) - (del (1+ (gethash (cons (1- i1) i2) table))) - (sub (gethash (cons (1- i1) (1- i2)) table))) - (unless (= (aref s1 (1- i1)) (aref s2 (1- i2))) - (cl-incf sub)) - (puthash (cons i1 i2) (min ins del sub) table)))) - table)) - -(defun apheleia--align-point (s1 s2 p1) - "Given strings S1 and S2 and index P1 in S1, return matching index P2 in S2. -If S1 and S2 are the same, then P1 and P2 will also be the same. -Otherwise, the text of S2 surrounding P2 is \"similar\" to the -text of S1 surrounding P1." - (let* ((table (apheleia--edit-distance-table s1 s2)) - (i1 (length s1)) - (i2 (length s2))) - (while (> i1 p1) - (let ((ins (1+ (gethash (cons i1 (1- i2)) table))) - (del (1+ (gethash (cons (1- i1) i2) table))) - (sub (gethash (cons (1- i1) (1- i2)) table))) - (unless (= (aref s1 (1- i1)) (aref s2 (1- i2))) - (cl-incf sub)) - (let ((cost (min ins del sub))) - (cond - ((= cost ins) - (cl-decf i2)) - ((= cost del) - (cl-decf i1)) - ((= cost sub) - (cl-decf i1) - (cl-decf i2)))))) - i2)) - -(defun apheleia--map-rcs-patch (func) - "Map over the RCS patch in the current buffer. -For each RCS patch command, FUNC is called with an alist that has -the following keys: - -- `command': either `addition' or `deletion' -- `start': line number, an integer -- `lines': number of lines to be inserted or removed -- `text': the string to be inserted, only for `addition' - -See -for documentation on the RCS patch format." - (save-excursion - (goto-char (point-min)) - (while (not (= (point) (point-max))) - (unless (looking-at "$\\|\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)") - (error "Malformed RCS patch: %S" (point))) - (forward-line) - (when-let ((command (match-string 1))) - (let ((start (string-to-number (match-string 2))) - (lines (string-to-number (match-string 3)))) - (pcase command - ("a" - (let ((text-start (point))) - (forward-line lines) - (funcall - func - `((command . addition) - (start . ,start) - (lines . ,lines) - (text . ,(buffer-substring-no-properties - text-start (point))))))) - ("d" - (funcall - func - `((command . deletion) - (start . ,start) - (lines . ,lines)))))))))) - -(defcustom apheleia-max-alignment-size 400 - "Maximum size for diff regions that will have point aligned. -Apheleia uses a dynamic programming algorithm to determine where -point should be placed within a diff region, but this algorithm -has quadratic runtime so it will lock up Emacs if it is run on a -diff region that is too large. The value of this variable serves -as a limit on the input size to the algorithm; larger diff -regions will still be applied, but Apheleia won't try to move -point correctly." - :type 'integer) - -(defun apheleia--apply-rcs-patch (content-buffer patch-buffer) - "Apply RCS patch. -CONTENT-BUFFER contains the text to be patched, and PATCH-BUFFER -contains the patch." - (let ((commands nil) - (point-list nil) - (window-line-list nil)) - (with-current-buffer content-buffer - (push (cons nil (point)) point-list) - (dolist (w (get-buffer-window-list nil nil t)) - (push (cons w (window-point w)) point-list) - (push (cons w (count-lines (window-start w) (point))) - window-line-list))) - (with-current-buffer patch-buffer - (apheleia--map-rcs-patch - (lambda (command) - (with-current-buffer content-buffer - ;; Could be optimized significantly by moving only as many - ;; lines as needed, rather than returning to the beginning - ;; of the buffer first. - (save-excursion - (goto-char (point-min)) - (forward-line (1- (alist-get 'start command))) - ;; Account for the off-by-one error in the RCS patch spec - ;; (namely, text is added *after* the line mentioned in - ;; the patch). - (when (eq (alist-get 'command command) 'addition) - (forward-line)) - (push `(marker . ,(point-marker)) command) - (push command commands) - ;; If we delete a region just before inserting new text - ;; at the same place, then it is a replacement. In this - ;; case, check if the replaced region includes the window - ;; point for any window currently displaying the content - ;; buffer. If so, figure out where that window point - ;; should be moved to, and record the information in an - ;; additional command. - ;; - ;; See . - ;; - ;; Note that the commands get pushed in reverse order - ;; because of how linked lists work. - (let ((deletion (nth 1 commands)) - (addition (nth 0 commands))) - (when (and (eq (alist-get 'command deletion) 'deletion) - (eq (alist-get 'command addition) 'addition) - ;; Again with the weird off-by-one - ;; computations. For example, if you replace - ;; lines 68 through 71 inclusive, then the - ;; deletion is for line 68 and the addition - ;; is for line 70. Blame RCS. - (= (+ (alist-get 'start deletion) - (alist-get 'lines deletion) - -1) - (alist-get 'start addition))) - (let ((text-start (alist-get 'marker deletion))) - (forward-line (alist-get 'lines deletion)) - (let ((text-end (point))) - (dolist (entry point-list) - ;; Check if the (window) point is within the - ;; replaced region. - (cl-destructuring-bind (w . p) entry - (when (and (< text-start p) - (< p text-end)) - (let* ((old-text (buffer-substring-no-properties - text-start text-end)) - (new-text (alist-get 'text addition)) - (old-relative-point (- p text-start)) - (new-relative-point - (if (> (max (length old-text) - (length new-text)) - apheleia-max-alignment-size) - old-relative-point - (apheleia--align-point - old-text new-text old-relative-point)))) - (goto-char text-start) - (push `((marker . ,(point-marker)) - (command . set-point) - (window . ,w) - (relative-point . ,new-relative-point)) - commands)))))))))))))) - (with-current-buffer content-buffer - (let ((move-to nil)) - (save-excursion - (dolist (command (nreverse commands)) - (goto-char (alist-get 'marker command)) - (pcase (alist-get 'command command) - (`addition - (insert (alist-get 'text command))) - (`deletion - (let ((text-start (point))) - (forward-line (alist-get 'lines command)) - (delete-region text-start (point)))) - (`set-point - (let ((new-point - (+ (point) (alist-get 'relative-point command)))) - (if-let ((w (alist-get 'window command))) - (set-window-point w new-point) - (setq move-to new-point))))))) - (when move-to - (goto-char move-to)))) - ;; Restore the scroll position of each window displaying the - ;; buffer. - (dolist (entry window-line-list) - (cl-destructuring-bind (w . old-window-line) entry - (let ((new-window-line - (count-lines (window-start w) (point)))) - (with-selected-window w - ;; Sometimes if the text is less than a buffer long, and - ;; we do a deletion, it might not be possible to keep the - ;; vertical position of point the same by scrolling. - ;; That's okay. We just go as far as we can. - (ignore-errors - (scroll-down (- old-window-line new-window-line))))))))) - -(defvar-local apheleia--current-process nil - "Current process that Apheleia is running, or nil. -Keeping track of this helps avoid running more than one process -at once.") - -(defvar apheleia--last-error-marker nil - "Marker for the last error message for any formatter. -This points into a log buffer.") - -(cl-defun apheleia--make-process - (&key name stdin stdout stderr command - remote noquery connection-type callback) - "Helper to run a formatter process asynchronously. -This starts a formatter process using COMMAND and then connects -STDIN, STDOUT and STDERR buffers to the processes different -streams. Once the process is finished CALLBACK will be invoked -with the exit-code of the formatter process as well as a boolean -saying whether the process was interrupted before completion. -REMOTE if supplied will be passed as the FILE-HANDLER argument to -`make-process'. - -See `make-process' for a description of the NAME and NOQUERY arguments." - (let ((proc - (make-process - :name name - :buffer stdout - :stderr stderr - :command command - :file-handler remote - :noquery noquery - :connection-type connection-type - :sentinel - (lambda (proc _event) - (unless (process-live-p proc) - (funcall - callback - (process-exit-status proc) - (process-get proc :interrupted))))))) - (set-process-sentinel (get-buffer-process stderr) #'ignore) - (when stdin - (set-process-coding-system - proc - nil - (buffer-local-value 'buffer-file-coding-system stdin)) - (process-send-string - proc - (with-current-buffer stdin - (buffer-string)))) - (process-send-eof proc) - proc)) - -(cl-defun apheleia--call-process - (&key name stdin stdout stderr command - remote noquery connection-type callback) - "Helper to synchronously run a formatter process. -This function essentially runs COMMAND synchronously passing STDIN -as standard input and saving output to the STDOUT and STDERR buffers. -Once the process is finished CALLBACK will be invoked with the exit -code (see `process-exit-status') of the process. - -This function accepts all the same arguments as `apheleia--make-process' -for simplicity, however some may not be used. This includes: NAME, -NO-QUERY, and CONNECTION-TYPE." - (ignore name noquery connection-type) - (let* ((run-on-remote (and (eq apheleia-remote-algorithm 'remote) - remote)) - (stderr-file (apheleia--make-temp-file run-on-remote "apheleia")) - (args - (append - (list - ;; argv[0] - (car command) - ;; If stdin we don't delete the STDIN buffer text with - ;; `call-process-region'. Otherwise we send no INFILE - ;; argument to `call-process'. - (not stdin) - ;; stdout buffer and stderr file. `call-process' cannot - ;; capture stderr into a separate buffer, the best we can - ;; do is save and read from a file. - `(,stdout ,stderr-file) - ;; Do not re/display stdout as output is recieved. - nil) - ;; argv[1:] - (cdr command)))) - (unwind-protect - (let ((exit-status - (cl-letf* ((message (symbol-function #'message)) - ((symbol-function #'message) - (lambda (format-string &rest args) - (unless (string-prefix-p "Renaming" (car args)) - (apply message format-string args))))) - (cond - ((and run-on-remote stdin) - ;; There's no call-process variant for this, we'll have to - ;; copy STDIN to a remote temporary file, create a subshell - ;; on the remote that runs the formatter and passes the temp - ;; file as stdin and then deletes it. - (let* ((remote-stdin - (apheleia--make-temp-file - run-on-remote "apheleia-stdin")) - ;; WARN: This assumes a POSIX compatible shell. - (shell - (or (bound-and-true-p tramp-default-remote-shell) - "sh")) - (shell-command - (concat - (mapconcat #'shell-quote-argument command " ") - " < " - (shell-quote-argument - (apheleia--strip-remote remote-stdin))))) - (unwind-protect - (progn - (with-current-buffer stdin - (apheleia--write-region-silently - nil nil remote-stdin)) - - (process-file - shell nil (nth 2 args) nil "-c" shell-command)) - (delete-file remote-stdin)))) - (stdin - (with-current-buffer stdin - (apply #'call-process-region - (point-min) (point-max) args))) - (run-on-remote - (apply #'process-file args)) - (t - (apply #'call-process args)))))) - ;; Save stderr from STDERR-FILE back into the STDERR buffer. - (with-current-buffer stderr - (insert-file-contents stderr-file)) - ;; I don't think it's possible to get here if the process - ;; was interrupted, since we were running it synchronously, - ;; so it should be ok to assume we pass nil to the callback. - (funcall callback exit-status nil) - ;; We return nil because there's no live process that can be - ;; returned. - nil) - (delete-file stderr-file)))) - -(cl-defun apheleia--execute-formatter-process - (&key command stdin remote callback ensure exit-status formatter) - "Wrapper for `make-process' that behaves a bit more nicely. -COMMAND is as in `make-process'. STDIN, if given, is a buffer -whose contents are fed to the process on stdin. CALLBACK is -invoked with one argument, the buffer containing the text from -stdout, when the process terminates (if it succeeds). ENSURE is a -callback that's invoked whether the process exited sucessfully or -not. EXIT-STATUS is a function which is called with the exit -status of the command; it should return non-nil to indicate that -the command succeeded. If EXIT-STATUS is omitted, then the -command succeeds provided that its exit status is 0. FORMATTER is -the symbol of the formatter that is being run, for diagnostic -purposes. FORMATTER is nil if the command being run does not -correspond to a formatter. REMOTE if non-nil will use the -formatter buffers file-handler, allowing the process to be -spawned on remote machines." - (when (process-live-p apheleia--current-process) - (message "Interrupting %s" apheleia--current-process) - (process-put apheleia--current-process :interrupted t) - (interrupt-process apheleia--current-process) - (accept-process-output apheleia--current-process 0.1 nil 'just-this-one) - (when (process-live-p apheleia--current-process) - (kill-process apheleia--current-process))) - (let* ((name (file-name-nondirectory (car command))) - (stdout (generate-new-buffer - (format " *apheleia-%s-stdout*" name))) - (stderr (generate-new-buffer - (format " *apheleia-%s-stderr*" name))) - (log-name (format "%s*apheleia-%s-log*" - (if apheleia-hide-log-buffers - " " - "") - name))) - (condition-case-unless-debug e - (progn - (setq apheleia--current-process - (funcall - (if remote #'apheleia--call-process #'apheleia--make-process) - :name (format "apheleia-%s" name) - :stdin stdin - :stdout stdout - :stderr stderr - :command command - :remote remote - :connection-type 'pipe - :noquery t - :callback - (lambda (proc-exit-status proc-interrupted) - (let ((exit-ok (and - (not proc-interrupted) - (funcall - (or exit-status #'zerop) - proc-exit-status)))) - ;; Append standard-error from current formatter - ;; to log buffer when - ;; `apheleia-log-only-errors' is nil or the - ;; formatter failed. Every process output is - ;; delimited by a line-feed character. - (unless (and exit-ok apheleia-log-only-errors) - (with-current-buffer (get-buffer-create log-name) - (special-mode) - (save-restriction - (widen) - (let ((inhibit-read-only t) - (orig-point (point)) - (keep-at-end (eobp)) - (stderr-string - (with-current-buffer stderr - (string-trim (buffer-string))))) - (goto-char (point-max)) - (skip-chars-backward "\n") - (delete-region (point) (point-max)) - (unless (bobp) - (insert - "\n\n\C-l\n")) - (unless exit-ok - (unless apheleia--last-error-marker - (setq apheleia--last-error-marker - (make-marker)) - (move-marker - apheleia--last-error-marker (point)))) - (insert - (current-time-string) - " :: " - (buffer-local-value 'default-directory stdout) - "\n$ " - (mapconcat #'shell-quote-argument command " ") - "\n\n" - (if (string-empty-p stderr-string) - "(no output on stderr)" - stderr-string) - "\n\n" - "Command " - (if exit-ok "succeeded" "failed") - " with exit code " - (number-to-string proc-exit-status) - ".\n") - ;; Known issue: this does not actually - ;; work; point is left at the end of - ;; the previous command output, instead - ;; of being moved to the end of the - ;; buffer for some reason. - (goto-char - (if keep-at-end - (point-max) - (min - (point-max) - orig-point))) - (goto-char (point-max)))))) - (when formatter - (run-hook-with-args - 'apheleia-formatter-exited-hook - :formatter formatter - :error (not exit-ok) - :log (get-buffer log-name))) - (unwind-protect - (if exit-ok - (when callback - (funcall callback stdout)) - (message - (concat - "Failed to run %s: exit status %s " - "(see %s %s)") - (car command) - proc-exit-status - (if (string-prefix-p " " log-name) - "hidden buffer" - "buffer") - (string-trim log-name))) - (when ensure - (funcall ensure)) - (ignore-errors - (kill-buffer stdout)) - (ignore-errors - (kill-buffer stderr)))))))) - (error - (ignore-errors - (kill-buffer stdout)) - (ignore-errors - (kill-buffer stderr)) - (message "Failed to run %s: %s" name (error-message-string e)))))) - -(defun apheleia-goto-error () - "Go to the most recently reported formatter error message." - (interactive) - (unless apheleia--last-error-marker - (user-error "No error has happened yet")) - (pop-to-buffer (marker-buffer apheleia--last-error-marker)) - (goto-char apheleia--last-error-marker)) - -(defun apheleia--write-region-silently - (start end filename &optional - append visit lockname mustbenew write-region) - "Like `write-region', but silent. -START, END, FILENAME, APPEND, VISIT, LOCKNAME, and MUSTBENEW are -as in `write-region'. WRITE-REGION is used instead of the actual -`write-region' function, if provided." - (funcall (or write-region #'write-region) - start end filename append 0 lockname mustbenew) - (when (or (eq visit t) (stringp visit)) - (setq buffer-file-name (if (eq visit t) - filename - visit)) - (set-visited-file-modtime) - (set-buffer-modified-p nil))) - -(defun apheleia--save-buffer-silently () - "Save the current buffer to its backing file, silently." - (cl-letf* ((write-region (symbol-function #'write-region)) - ((symbol-function #'write-region) - (lambda (start end filename &optional - append visit lockname mustbenew) - (apheleia--write-region-silently - start end filename append visit - lockname mustbenew write-region))) - (message (symbol-function #'message)) - ((symbol-function #'message) - (lambda (format &rest args) - (unless (equal format "Saving file %s...") - (apply message format args)))) - ;; Avoid triggering `after-set-visited-file-name-hook', - ;; which can have various undesired effects in particular - ;; major modes. Unfortunately, `write-file' triggers this - ;; hook unconditionally even if the filename was not - ;; changed, hence this hack :/ - (run-hooks (symbol-function #'run-hooks)) - ((symbol-function #'run-hooks) - (lambda (&rest args) - (unless (equal args '(after-set-visited-file-name-hook)) - (apply run-hooks args))))) - (save-buffer))) - -(defun apheleia--strip-remote (file-name) - "Return FILE-NAME with any TRAMP prefix removed. -If FILE-NAME is not remote, return it unchanged." - (if-let ((remote (file-remote-p file-name))) - (substring file-name (length remote)) - file-name)) - -(defun apheleia--make-temp-file (remote prefix &optional dir-flag suffix) - "Create a temporary file optionally on a remote machine. -This function calls `make-temp-file' or `make-nearby-temp-file' depending on -the value of REMOTE. - -See `make-temp-file' for a description of PREFIX, DIR-FLAG, and SUFFIX." - (funcall - (if remote - #'make-nearby-temp-file - #'make-temp-file) - prefix dir-flag suffix)) - -(defun apheleia--create-rcs-patch (old-buffer new-buffer remote callback) - "Generate RCS patch from text in OLD-BUFFER to text in NEW-BUFFER. -Once finished, invoke CALLBACK with a buffer containing the patch -as its sole argument. - -See `apheleia--run-formatters' for a description of REMOTE." - ;; Make sure at least one of the two buffers is saved to a file. The - ;; other one we can feed on stdin. - (let ((old-fname - (with-current-buffer old-buffer - (and (not (buffer-modified-p)) buffer-file-name))) - (new-fname - (with-current-buffer new-buffer - (and (not (buffer-modified-p)) buffer-file-name))) - ;; Place any temporary files we must delete in here. - (clear-files nil) - (run-on-remote (and (eq apheleia-remote-algorithm 'remote) - remote))) - (cl-labels ((;; Weird indentation because of differences in Emacs - ;; indentation algorithm between 27 and 28 - apheleia--make-temp-file-for-rcs-patch - (buffer &optional fname) - ;; Ensure there's a file with the contents of `buffer' on the - ;; target machine. `fname', if given, refers to an existing - ;; file that may not exist on the target machine and needs - ;; to be copied over. - (let ((fname-remote (and fname (file-remote-p fname)))) - (when (or (not fname) - (not (equal run-on-remote fname-remote))) - (setq fname - (apheleia--make-temp-file run-on-remote "apheleia")) - (push fname clear-files) - (with-current-buffer buffer - (apheleia--write-region-silently - (point-min) (point-max) fname))) - (apheleia--strip-remote fname)))) - ;; Ensure file is on target right machine, or create a copy of it. - (when old-fname - (setq old-fname - (apheleia--make-temp-file-for-rcs-patch old-buffer old-fname))) - (when new-fname - (setq new-fname - (apheleia--make-temp-file-for-rcs-patch new-buffer new-fname))) - ;; When neither files have an open file-handle, create one. - (unless (or old-fname new-fname) - (setq new-fname (apheleia--make-temp-file-for-rcs-patch new-buffer)))) - - (apheleia--execute-formatter-process - :command `("diff" "--rcs" "--strip-trailing-cr" "--" - ,(or old-fname "-") - ,(or new-fname "-")) - :stdin (if new-fname old-buffer new-buffer) - :callback callback - :remote remote - :ensure - (lambda () - (dolist (file clear-files) - (ignore-errors - (delete-file file)))) - :exit-status (lambda (status) - ;; Exit status is 0 if no changes, 1 if some - ;; changes, and 2 if error. - (memq status '(0 1)))))) - -(defun apheleia--safe-buffer-name () - "Return `buffer-name' without special file-system characters." - ;; See https://stackoverflow.com/q/1976007 for a list of supported - ;; characters on all systems. - (replace-regexp-in-string - (rx (or "/" "<" ">" ":" "\"" "\\" "|" "?" "*")) - "" - (buffer-name))) - -(defun apheleia--format-command (command remote &optional stdin-buffer) - "Format COMMAND into a shell command and list of file paths. -Returns a list with the car being the optional input file-name, the -cadr being the optional output file-name, the caddr is the buffer to -send as stdin to the formatter (when the input-fname is not used), -and the cdddr being the cmd to run. - -STDIN-BUFFER is the optional buffer to use when creating a temporary -file for the formatters standard input. REMOTE asserts whether the -buffer being formatted is on a remote machine or the local machine. -See `apheleia--run-formatters' for more details on the usage of REMOTE. - -If COMMAND uses the symbol `file' and the current buffer is modified -from what is written to disk, then return nil meaning meaning no -cmd is to be run." - (cl-block nil - (let* ((input-fname nil) - (output-fname nil) - ;; Either we're running remotely and the buffer is - ;; remote, or we're not running remotely and the - ;; buffer isn't remote. - (run-on-remote - (and (eq apheleia-remote-algorithm 'remote) - remote)) - ;; Whether the machine the process will run on matches - ;; the machine the buffer/file is currently on. Either - ;; we're running remotely and the buffer is remote or - ;; we're not running remotely and the buffer is not - ;; remote. - (remote-match (equal run-on-remote remote)) - (stdin (or stdin-buffer (current-buffer))) - (npx nil) - (command (apply #'list command))) - ;; TODO: Support arbitrary package managers, not just NPM. - (when (memq 'npx command) - (setq npx t) - (setq command (remq 'npx command))) - (when (and npx remote-match) - (when-let ((project-dir - (locate-dominating-file - default-directory "node_modules"))) - (let ((binary - (expand-file-name - (car command) - (expand-file-name - ".bin" - (expand-file-name - "node_modules" - project-dir))))) - (when (file-executable-p binary) - (setcar command binary))))) - (when (or (memq 'file command) (memq 'filepath command)) - ;; Fail when using file but not as the first formatter in this - ;; sequence. (But filepath is okay, since it indicates content - ;; is not actually being read from the named file.) - (when (memq 'file command) - (when stdin-buffer - (error "Cannot run formatter using `file' in a sequence unless \ -it's first in the sequence")) - (unless remote-match - (error "Formatter uses `file' but process will run on different \ -machine from the machine file is available on")) - (setq stdin nil) - ;; If `buffer-file-name' is nil then there is no backing - ;; file, so `buffer-modified-p' should be ignored (it always - ;; returns non-nil). - (when (and (buffer-modified-p) buffer-file-name) - (cl-return))) - ;; We always strip out the remote-path prefix for file/filepath. - (let ((file-name (apheleia--strip-remote - (or buffer-file-name - (concat default-directory - (apheleia--safe-buffer-name)))))) - (setq command (mapcar (lambda (arg) - (if (memq arg '(file filepath)) - file-name - arg)) - command)))) - (when (or (memq 'input command) (memq 'inplace command)) - (setq input-fname (apheleia--make-temp-file - run-on-remote "apheleia" nil - (when-let ((file-name - (or buffer-file-name - (apheleia--safe-buffer-name)))) - (file-name-extension file-name 'period)))) - (with-current-buffer stdin - (apheleia--write-region-silently nil nil input-fname)) - (let ((input-fname (apheleia--strip-remote input-fname))) - (setq command (mapcar (lambda (arg) - (if (memq arg '(input inplace)) - (progn - (setq output-fname input-fname) - input-fname) - arg)) - command)))) - (when (memq 'output command) - (setq output-fname (apheleia--make-temp-file run-on-remote "apheleia")) - (let ((output-fname (apheleia--strip-remote output-fname))) - (setq command (mapcar (lambda (arg) - (if (eq arg 'output) - output-fname - arg)) - command)))) - ;; Evaluate each element of arg that isn't a string and replace - ;; it with the evaluated value. The result of an evaluation should - ;; be a string or a list of strings. If the former its replaced as - ;; is. If the latter the contents of the list is substituted in - ;; place. - (setq command - (cl-loop - for arg in command - with val = nil - do (setq val (if (stringp arg) - arg - (eval arg))) - if val - if (and (consp val) - (cl-every #'stringp val)) - append val - else if (stringp val) - collect val - else do (error "Result of command evaluation must be a string \ -or list of strings: %S" arg))) - `(,input-fname ,output-fname ,stdin ,@command)))) - -(defun apheleia--run-formatter-process - (command buffer remote callback stdin formatter) - "Run a formatter using a shell command. -COMMAND should be a list of string or symbols for the formatter that -will format the current buffer. See `apheleia--run-formatters' for a -description of COMMAND, BUFFER, CALLBACK, REMOTE, and STDIN. FORMATTER -is the symbol of the current formatter being run, for diagnostic -purposes." - ;; NOTE: We switch to the original buffer both to format the command - ;; correctly and also to ensure any buffer local variables correctly - ;; resolve for the whole formatting process (for example - ;; `apheleia--current-process'). - (with-current-buffer buffer - (when-let ((ret (apheleia--format-command command remote stdin)) - (exec-path - (append `(,(expand-file-name - "scripts/formatters" - (file-name-directory - (file-truename - ;; Borrowed with love from Magit - (let ((load-suffixes '(".el"))) - (locate-library "apheleia")))))) - exec-path))) - (cl-destructuring-bind (input-fname output-fname stdin &rest command) ret - (when (executable-find (car command)) - (apheleia--execute-formatter-process - :command command - :stdin (unless input-fname - stdin) - :callback - (lambda (stdout) - (when output-fname - ;; Load output-fname contents into the stdout buffer. - (with-current-buffer stdout - (erase-buffer) - (insert-file-contents-literally output-fname))) - (funcall callback stdout)) - :ensure - (lambda () - (ignore-errors - (when input-fname - (delete-file input-fname)) - (when output-fname - (delete-file output-fname)))) - :remote remote - :formatter formatter)))))) - -(defun apheleia--run-formatter-function - (func buffer remote callback stdin formatter) - "Run a formatter using a Lisp function FUNC. -See `apheleia--run-formatters' for a description of BUFFER, REMOTE, -CALLBACK and STDIN. FORMATTER is the symbol of the current formatter -being run, for diagnostic purposes." - (let* ((formatter-name (if (symbolp func) (symbol-name func) "lambda")) - (scratch (generate-new-buffer - (format " *apheleia-%s-scratch*" formatter-name)))) - (with-current-buffer scratch - ;; We expect FUNC to modify scratch in place so we can't simply pass - ;; STDIN to it. When STDIN isn't nil, it's the output of a previous - ;; formatter and we want to keep it alive so we can debug any issues - ;; with it. - (insert-buffer-substring (or stdin buffer)) - (funcall func - ;; Original buffer being formatted. - :buffer buffer - ;; Buffer the formatter should modify. - :scratch scratch - ;; Name of the current formatter symbol. - :formatter formatter - ;; Callback after succesfully formatting. - :callback - (lambda () - (unwind-protect - (funcall callback scratch) - (kill-buffer scratch))) - ;; The remote part of the buffers file-name or directory. - :remote remote - ;; Whether the formatter should be run async or not. - :async (not remote) - ;; Callback when formatting scratch has failed. - :callback - (apply-partially #'kill-buffer scratch))))) - -(defcustom apheleia-formatters - '((bean-format . ("bean-format")) - (black . ("black" "-")) - (brittany . ("brittany")) - (caddyfmt . ("caddy" "fmt" "-")) - (clang-format . ("clang-format" - "-assume-filename" - (or (buffer-file-name) - (cdr (assoc major-mode - '((c-mode . ".c") - (c++-mode . ".cpp") - (cuda-mode . ".cu") - (protobuf-mode . ".proto")))) - ".c"))) - (crystal-tool-format . ("crystal" "tool" "format" "-")) - (dart-format . ("dart" "format")) - (elm-format . ("elm-format" "--yes" "--stdin")) - (fish-indent . ("fish_indent")) - (gofmt . ("gofmt")) - (gofumpt . ("gofumpt")) - (goimports . ("goimports")) - (google-java-format . ("google-java-format" "-")) - (isort . ("isort" "-")) - (lisp-indent . apheleia-indent-lisp-buffer) - (ktlint . ("ktlint" "--stdin" "-F")) - (latexindent . ("latexindent" "--logfile=/dev/null")) - (mix-format . ("mix" "format" "-")) - (nixfmt . ("nixfmt")) - (ocamlformat . ("ocamlformat" "-" "--name" filepath - "--enable-outside-detected-project")) - (phpcs . ("apheleia-phpcs")) - (prettier . (npx "prettier" "--stdin-filepath" filepath)) - (prettier-css - . (npx "prettier" "--stdin-filepath" filepath "--parser=css")) - (prettier-html - . (npx "prettier" "--stdin-filepath" filepath "--parser=html")) - (prettier-graphql - . (npx "prettier" "--stdin-filepath" filepath "--parser=graphql")) - (prettier-javascript - . (npx "prettier" "--stdin-filepath" filepath "--parser=babel-flow")) - (prettier-json - . (npx "prettier" "--stdin-filepath" filepath "--parser=json")) - (prettier-markdown - . (npx "prettier" "--stdin-filepath" filepath "--parser=markdown")) - (prettier-ruby - . (npx "prettier" "--stdin-filepath" filepath "--parser=ruby")) - (prettier-scss - . (npx "prettier" "--stdin-filepath" filepath "--parser=scss")) - (prettier-typescript - . (npx "prettier" "--stdin-filepath" filepath "--parser=typescript")) - (prettier-yaml - . (npx "prettier" "--stdin-filepath" filepath "--parser=yaml")) - (shfmt . ("shfmt" "-i" "4")) - (stylua . ("stylua" "-")) - (rustfmt . ("rustfmt" "--quiet" "--emit" "stdout")) - (terraform . ("terraform" "fmt" "-"))) - "Alist of code formatting commands. -The keys may be any symbols you want, and the values are shell -commands, lists of strings and symbols, or a function symbol. - -If the value is a function, the function will be called with -keyword arguments (see the implementation of -`apheleia--run-formatter-function' to see which). It should use -`cl-defun' with `&allow-other-keys' for forward compatibility. - -Otherwise in Lisp code, the format of commands is similar to what -you pass to `make-process', except as follows. - -Normally, the contents of the current buffer are passed to the -command on stdin, and the output is read from stdout. However, if -you use the symbol `file' as one of the elements of commands, -then the filename of the current buffer is substituted for -it. (Use `filepath' instead of `file' if you need the filename of -the current buffer, but you still want its contents to be passed -on stdin.) - -If you instead use the symbol `input' as one of the elements of -commands, then the contents of the current buffer are written to -a temporary file and its name is substituted for `input'. Also, -if you use the symbol `output' as one of the elements of -commands, then it is substituted with the name of a temporary -file. In that case, it is expected that the command writes to -that file, and the file is then read into an Emacs buffer. - -If you use the symbol `inplace' as one of the elements of the -list, then the contents of the current buffer are written to a -temporary file and its name is substituted for `inplace'. -However, unlike `input', it is expected that the formatter write -the formatted file back to the same file in place. In other -words, `inplace' is like `input' and `output' together. - -If you use the symbol `npx' as one of the elements of commands, -then the first string element of the command list is resolved -inside node_modules/.bin if such a directory exists anywhere -above the current `default-directory'. - -Any list elements that are not strings and not any of the special -symbols mentioned above will be evaluated when the formatter is -invoked, and spliced into the list. A form can evaluate either to -a string or to a list of strings. - -The \"scripts/formatters\" subdirectory of the Apheleia source -repository is automatically prepended to $PATH (variable -`exec-path', to be specific) when invoking external formatters. -This is intended for internal use. If you would like to define -your own script, you can simply place it on your normal $PATH -rather than using this system." - :type '(alist - :key-type symbol - :value-type - (choice - (repeat - (choice - (string :tag "Argument") - (const :tag "Look for command in node_modules/.bin" npx) - (const :tag "Name of file being formatted" filepath) - (const :tag "Name of real file used for input" file) - (const :tag "Name of temporary file used for input" input) - (const :tag "Name of temporary file used for output" output))) - (function :tag "Formatter function")))) - -(cl-defun apheleia-indent-lisp-buffer - (&key buffer scratch callback &allow-other-keys) - "Format a Lisp BUFFER. -Use SCRATCH as a temporary buffer and CALLBACK to apply the -transformation. - -For more implementation detail, see -`apheleia--run-formatter-function'." - (with-current-buffer scratch - (setq-local indent-line-function - (buffer-local-value 'indent-line-function buffer)) - (setq-local lisp-indent-function - (buffer-local-value 'lisp-indent-function buffer)) - (funcall (with-current-buffer buffer major-mode)) - (goto-char (point-min)) - (let ((inhibit-message t) - (message-log-max nil)) - (indent-region (point-min) (point-max))) - (funcall callback))) - -(defun apheleia--run-formatters - (formatters buffer remote callback &optional stdin) - "Run one or more code formatters on the current buffer. -FORMATTERS is a list of symbols that appear as keys in -`apheleia-formatters'. BUFFER is the `current-buffer' when this -function was first called. Once all the formatters in COMMANDS -finish succesfully then invoke CALLBACK with one argument, a -buffer containing the output of all the formatters. REMOTE asserts -whether the buffer being formatted is on a remote machine or the -current machine. It should be the output of `file-remote-p' on the -current variable `buffer-file-name'. REMOTE is the remote part of the -original buffers file-name or directory'. It's used alongside -`apheleia-remote-algorithm' to determine where the formatter process -and any temporary files it may need should be placed. - -STDIN is a buffer containing the standard input for the first -formatter in COMMANDS. This should not be supplied by the caller -and instead is supplied by this command when invoked recursively. -The stdout of the previous formatter becomes the stdin of the -next formatter." - (let ((command (alist-get (car formatters) apheleia-formatters))) - (funcall - (cond - ((consp command) - #'apheleia--run-formatter-process) - ((or (functionp command) - (symbolp command)) - #'apheleia--run-formatter-function) - (t - (error "Formatter must be a shell command or a Lisp \ -function: %s" command))) - command - buffer - remote - (lambda (stdout) - (unless (string-empty-p (with-current-buffer stdout (buffer-string))) - (if (cdr formatters) - ;; Forward current stdout to remaining formatters, passing along - ;; the current callback and using the current formatters output - ;; as stdin. - (apheleia--run-formatters - (cdr formatters) buffer remote callback stdout) - (funcall callback stdout)))) - stdin - (car formatters)))) - -(defcustom apheleia-mode-alist - '(;; php-mode has to come before cc-mode - (php-mode . phpcs) - ;; json-mode has to come before javascript-mode (aka js-mode) - (json-mode . prettier-json) - (json-ts-mode . prettier-json) - ;; rest are alphabetical - (bash-ts-mode . shfmt) - (beancount-mode . bean-format) - (c++-ts-mode . clang-format) - (caddyfile-mode . caddyfmt) - (cc-mode . clang-format) - (c-mode . clang-format) - (c-ts-mode . clang-format) - (c++-mode . clang-format) - (caml-mode . ocamlformat) - (common-lisp-mode . lisp-indent) - (crystal-mode . crystal-tool-format) - (css-mode . prettier-css) - (css-ts-mode . prettier-css) - (dart-mode . dart-format) - (elixir-mode . mix-format) - (elixir-ts-mode . mix-format) - (elm-mode . elm-format) - (fish-mode . fish-indent) - (go-mode . gofmt) - (go-mod-ts-mode . gofmt) - (go-ts-mode . gofmt) - (graphql-mode . prettier-graphql) - (haskell-mode . brittany) - (html-mode . prettier-html) - (java-mode . google-java-format) - (java-ts-mode . google-java-format) - (js3-mode . prettier-javascript) - (js-mode . prettier-javascript) - (js-ts-mode . prettier-javascript) - (kotlin-mode . ktlint) - (latex-mode . latexindent) - (LaTeX-mode . latexindent) - (lua-mode . stylua) - (lisp-mode . lisp-indent) - (nix-mode . nixfmt) - (python-mode . black) - (python-ts-mode . black) - (ruby-mode . prettier-ruby) - (ruby-ts-mode . prettier-ruby) - (rustic-mode . rustfmt) - (rust-mode . rustfmt) - (rust-ts-mode . rustfmt) - (scss-mode . prettier-scss) - (terraform-mode . terraform) - (TeX-latex-mode . latexindent) - (TeX-mode . latexindent) - (tsx-ts-mode . prettier-typescript) - (tuareg-mode . ocamlformat) - (typescript-mode . prettier-typescript) - (typescript-ts-mode . prettier-typescript) - (web-mode . prettier) - (yaml-mode . prettier-yaml) - (yaml-ts-mode . prettier-yaml)) - "Alist mapping major mode names to formatters to use in those modes. -This determines what formatter to use in buffers without a -setting for `apheleia-formatter'. The keys are major mode -symbols (matched against `major-mode' with `derived-mode-p') or -strings (matched against value of variable `buffer-file-name' -with `string-match-p'), and the values are symbols with entries -in `apheleia-formatters' (or equivalently, they are allowed -values for `apheleia-formatter'). Values can be a list of such -symnols causing each formatter in the list to be called one after -the other (with the output of the previous formatter). -Earlier entries in this variable take precedence over later ones. - -Be careful when writing regexps to include \"\\'\" and to escape -\"\\.\" in order to properly match a file extension. For example, -to match \".jsx\" files you might use \"\\.jsx\\'\". - -If a given mode derives from another mode (e.g. `php-mode' and -`cc-mode'), then ensure that the deriving mode comes before the mode -to derive from, as the list is interpreted sequentially." - :type '(alist - :key-type - (choice (symbol :tag "Major mode") - (string :tag "Buffer name regexp")) - :value-type - (choice (symbol :tag "Formatter") - (repeat - (symbol :tag "Formatter"))))) - -(defvar-local apheleia-formatter nil - "Name of formatter to use in current buffer, a symbol or nil. -If non-nil, then `apheleia-formatters' should have a matching -entry. This overrides `apheleia-mode-alist'.") -(put 'apheleia-formatter 'safe-local-variable 'symbolp) - -(defun apheleia--ensure-list (arg) - "Ensure ARG is a list of length at least 1. -When ARG is not a list its turned into a list." - (if (listp arg) - arg - (list arg))) - -(defun apheleia--get-formatters (&optional interactive) - "Return the list of formatters to use for the current buffer. -This is a list of symbols that may appear as cars in -`apheleia-formatters', or nil if no formatter is configured for -the current buffer. - -Consult the values of `apheleia-mode-alist' and -`apheleia-formatter' to determine which formatter is configured. - -If INTERACTIVE is non-nil, then prompt the user for which -formatter to run if none is configured, instead of returning nil. -If INTERACTIVE is the special symbol `prompt', then prompt -even if a formatter is configured." - (or (and (not (eq interactive 'prompt)) - (apheleia--ensure-list - (or apheleia-formatter - (cl-dolist (entry apheleia-mode-alist) - (when (or (and (symbolp (car entry)) - (derived-mode-p (car entry))) - (and (stringp (car entry)) - buffer-file-name - (string-match-p - (car entry) buffer-file-name))) - (cl-return (cdr entry))))))) - (and interactive - (list - (intern - (completing-read - "Formatter: " - (or (map-keys apheleia-formatters) - (user-error - "No formatters in `apheleia-formatters'")) - nil 'require-match)))))) - -(defun apheleia--buffer-hash () - "Compute hash of current buffer." - (if (fboundp 'buffer-hash) - (buffer-hash) - (md5 (current-buffer)))) - -(defvar apheleia--buffer-hash nil - "Return value of `buffer-hash' when formatter started running.") - -(defun apheleia--disallowed-p () - "Return an error message if Apheleia cannot be run, else nil." - (when (and buffer-file-name - (file-remote-p (or buffer-file-name - default-directory)) - (eq apheleia-remote-algorithm 'cancel)) - "Apheleia refused to run formatter due to `apheleia-remote-algorithm'")) - -;;;###autoload -(defun apheleia-format-buffer (formatter &optional callback) - "Run code formatter asynchronously on current buffer, preserving point. - -FORMATTER is a symbol appearing as a key in -`apheleia-formatters', or a list of them to run multiple -formatters in a chain. If called interactively, run the currently -configured formatters (see `apheleia-formatter' and -`apheleia-mode-alist'), or prompt from `apheleia-formatters' if -there is none configured for the current buffer. With a prefix -argument, prompt always. - -After the formatters finish running, the diff utility is invoked to -determine what changes it made. That diff is then used to apply the -formatter's changes to the current buffer without moving point or -changing the scroll position in any window displaying the buffer. If -the buffer has been modified since the formatter started running, -however, the operation is aborted. - -If the formatter actually finishes running and the buffer is -successfully updated (even if the formatter has not made any -changes), CALLBACK, if provided, is invoked with no arguments." - (interactive (progn - (when-let ((err (apheleia--disallowed-p))) - (user-error err)) - (list (apheleia--get-formatters - (if current-prefix-arg - 'prompt - 'interactive))))) - (let ((formatters (apheleia--ensure-list formatter))) - ;; Check for this error ahead of time so we don't have to deal - ;; with it anywhere in the internal machinery of Apheleia. - (dolist (formatter formatters) - (unless (alist-get formatter apheleia-formatters) - (user-error - "No such formatter defined in `apheleia-formatters': %S" - formatter))) - ;; Fail silently if disallowed, since we don't want to throw an - ;; error on `post-command-hook'. We already took care of throwing - ;; `user-error' on interactive usage above. - (unless (apheleia--disallowed-p) - (setq-local apheleia--buffer-hash (apheleia--buffer-hash)) - (let ((cur-buffer (current-buffer)) - (remote (file-remote-p (or buffer-file-name - default-directory)))) - (apheleia--run-formatters - formatters - cur-buffer - remote - (lambda (formatted-buffer) - (when (buffer-live-p cur-buffer) - (with-current-buffer cur-buffer - ;; Short-circuit. - (when - (equal - apheleia--buffer-hash (apheleia--buffer-hash)) - (apheleia--create-rcs-patch - cur-buffer formatted-buffer remote - (lambda (patch-buffer) - (when (buffer-live-p cur-buffer) - (with-current-buffer cur-buffer - (when - (equal - apheleia--buffer-hash (apheleia--buffer-hash)) - (apheleia--apply-rcs-patch - (current-buffer) patch-buffer) - (when callback - (funcall callback)))))))))))))))) - -(defcustom apheleia-post-format-hook nil - "Normal hook run after Apheleia formats a buffer successfully." - :type 'hook) - -(defcustom apheleia-inhibit-functions nil - "List of functions that prevent Apheleia from turning on automatically. -If one of these returns non-nil then `apheleia-mode' is not -enabled in a buffer, even if `apheleia-global-mode' is on. You -can still manually enable `apheleia-mode' in such a buffer. - -See also `apheleia-inhibit' for another way to accomplish a -similar task." - :type '(repeat function)) - -;; Handle recursive references. -(defvar apheleia-mode) - -;; Prevent infinite loop. -(defvar apheleia--format-after-save-in-progress nil - "Prevent `apheleia--format-after-save' from being called recursively. -This will be locally bound to t while `apheleia--format-after-save' is -operating, to prevent an infinite loop.") - -;; Autoload because the user may enable `apheleia-mode' without -;; loading Apheleia; thus this function may be invoked as an autoload. -;;;###autoload -(defun apheleia--format-after-save () - "Run code formatter for current buffer if any configured, then save." - (unless apheleia--format-after-save-in-progress - (when (and apheleia-mode (not (buffer-narrowed-p))) - (when-let ((formatters (apheleia--get-formatters))) - (apheleia-format-buffer - formatters - (lambda () - (with-demoted-errors "Apheleia: %s" - (when buffer-file-name - (let ((apheleia--format-after-save-in-progress t)) - (apheleia--save-buffer-silently))) - (run-hooks 'apheleia-post-format-hook)))))))) - -;; Use `progn' to force the entire minor mode definition to be copied -;; into the autoloads file, so that the minor mode can be enabled -;; without pulling in all of Apheleia during init. -;;;###autoload -(progn - - (define-minor-mode apheleia-mode - "Minor mode for reformatting code on save without moving point. -It is customized by means of the variables `apheleia-mode-alist' -and `apheleia-formatters'." - :lighter apheleia-mode-lighter - (if apheleia-mode - (add-hook 'after-save-hook #'apheleia--format-after-save nil 'local) - (remove-hook 'after-save-hook #'apheleia--format-after-save 'local))) - - - (defvar-local apheleia-inhibit nil - "Do not enable `apheleia-mode' automatically if non-nil. -This is designed for use in .dir-locals.el. - -See also `apheleia-inhibit-functions'.") - (put 'apheleia-inhibit 'safe-local-variable #'booleanp) - - (defun apheleia-mode-maybe () - "Enable `apheleia-mode' if allowed by user configuration. -This checks `apheleia-inhibit-functions' and `apheleia-inhibit' -to see if it is allowed." - (unless (or - apheleia-inhibit - (run-hook-with-args-until-success - 'apheleia-inhibit-functions)) - (apheleia-mode))) - - (define-globalized-minor-mode apheleia-global-mode - apheleia-mode apheleia-mode-maybe) - - (put 'apheleia-mode 'safe-local-variable #'booleanp)) - (provide 'apheleia) ;;; apheleia.el ends here From 23bc9f560f478c8a40c570251257b81837400142 Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Sat, 11 Mar 2023 18:27:23 +0000 Subject: [PATCH 2/8] Fix build errors related to previous commit --- Makefile | 2 + apheleia-core.el | 112 ++++++----------------------------------- apheleia-formatters.el | 8 +-- apheleia.el | 2 + 4 files changed, 25 insertions(+), 99 deletions(-) diff --git a/Makefile b/Makefile index b5aea8ff..bc4bee4f 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,8 @@ checkindent: ## Ensure that indentation is correct emacs -Q --batch \ -l scripts/apheleia-indent.el \ --eval "(setq inhibit-message t)" \ + --eval "(setq load-path \ + (append (list default-directory) load-path))" \ --eval "(load (expand-file-name \"apheleia.el\") nil t)" \ --eval "(find-file \"$$file\")" \ --eval "(indent-region (point-min) (point-max))" \ diff --git a/apheleia-core.el b/apheleia-core.el index 6a4cd2a8..9807af3f 100644 --- a/apheleia-core.el +++ b/apheleia-core.el @@ -25,13 +25,15 @@ Hidden buffers have names that begin with a space, and do not appear in `switch-to-buffer' unless you type in a space manually." - :type 'boolean) + :type 'boolean + :group 'apheleia) (defcustom apheleia-log-only-errors t "Non-nil means Apheleia will only log when an error occurs. Otherwise, Apheleia will log every time a formatter is run, even if it is successful." - :type 'boolean) + :type 'boolean + :group 'apheleia) (defcustom apheleia-formatter-exited-hook nil "Abnormal hook run after a formatter has finished running. @@ -48,7 +50,8 @@ none (e.g., because logging is not enabled). This hook is run before `apheleia-after-format-hook', and may be run multiple times if `apheleia-mode-alist' configures multiple formatters to run in a chain, with one run per formatter." - :type 'hook) + :type 'hook + :group 'apheleia) (defcustom apheleia-remote-algorithm 'cancel "How `apheleia' should process remote files/buffers. @@ -68,7 +71,8 @@ features of `apheleia' (such as `file' in `apheleia-formatters') is not compatible with this option and formatters relying on them will crash." :type '(choice (const :tag "Run the formatter on the local machine" local) (const :tag "Run the formatter on the remote machine" remote) - (const :tag "Disable formatting for remote buffers" cancel))) + (const :tag "Disable formatting for remote buffers" cancel)) + :group 'apheleia) (defcustom apheleia-mode-lighter " Apheleia" "Lighter for `apheleia-mode'." @@ -172,7 +176,8 @@ diff region that is too large. The value of this variable serves as a limit on the input size to the algorithm; larger diff regions will still be applied, but Apheleia won't try to move point correctly." - :type 'integer) + :type 'integer + :group 'apheleia) (defun apheleia--apply-rcs-patch (content-buffer patch-buffer) "Apply RCS patch. @@ -987,94 +992,6 @@ function: %s" command))) stdin (car formatters)))) -(defcustom apheleia-mode-alist - '(;; php-mode has to come before cc-mode - (php-mode . phpcs) - ;; json-mode has to come before javascript-mode (aka js-mode) - (json-mode . prettier-json) - (json-ts-mode . prettier-json) - ;; rest are alphabetical - (bash-ts-mode . shfmt) - (beancount-mode . bean-format) - (c++-ts-mode . clang-format) - (caddyfile-mode . caddyfmt) - (cc-mode . clang-format) - (c-mode . clang-format) - (c-ts-mode . clang-format) - (c++-mode . clang-format) - (caml-mode . ocamlformat) - (common-lisp-mode . lisp-indent) - (crystal-mode . crystal-tool-format) - (css-mode . prettier-css) - (css-ts-mode . prettier-css) - (dart-mode . dart-format) - (elixir-mode . mix-format) - (elixir-ts-mode . mix-format) - (elm-mode . elm-format) - (fish-mode . fish-indent) - (go-mode . gofmt) - (go-mod-ts-mode . gofmt) - (go-ts-mode . gofmt) - (graphql-mode . prettier-graphql) - (haskell-mode . brittany) - (html-mode . prettier-html) - (java-mode . google-java-format) - (java-ts-mode . google-java-format) - (js3-mode . prettier-javascript) - (js-mode . prettier-javascript) - (js-ts-mode . prettier-javascript) - (kotlin-mode . ktlint) - (latex-mode . latexindent) - (LaTeX-mode . latexindent) - (lua-mode . stylua) - (lisp-mode . lisp-indent) - (nix-mode . nixfmt) - (python-mode . black) - (python-ts-mode . black) - (ruby-mode . prettier-ruby) - (ruby-ts-mode . prettier-ruby) - (rustic-mode . rustfmt) - (rust-mode . rustfmt) - (rust-ts-mode . rustfmt) - (scss-mode . prettier-scss) - (terraform-mode . terraform) - (TeX-latex-mode . latexindent) - (TeX-mode . latexindent) - (tsx-ts-mode . prettier-typescript) - (tuareg-mode . ocamlformat) - (typescript-mode . prettier-typescript) - (typescript-ts-mode . prettier-typescript) - (web-mode . prettier) - (yaml-mode . prettier-yaml) - (yaml-ts-mode . prettier-yaml)) - "Alist mapping major mode names to formatters to use in those modes. -This determines what formatter to use in buffers without a -setting for `apheleia-formatter'. The keys are major mode -symbols (matched against `major-mode' with `derived-mode-p') or -strings (matched against value of variable `buffer-file-name' -with `string-match-p'), and the values are symbols with entries -in `apheleia-formatters' (or equivalently, they are allowed -values for `apheleia-formatter'). Values can be a list of such -symnols causing each formatter in the list to be called one after -the other (with the output of the previous formatter). -Earlier entries in this variable take precedence over later ones. - -Be careful when writing regexps to include \"\\'\" and to escape -\"\\.\" in order to properly match a file extension. For example, -to match \".jsx\" files you might use \"\\.jsx\\'\". - -If a given mode derives from another mode (e.g. `php-mode' and -`cc-mode'), then ensure that the deriving mode comes before the mode -to derive from, as the list is interpreted sequentially." - :type '(alist - :key-type - (choice (symbol :tag "Major mode") - (string :tag "Buffer name regexp")) - :value-type - (choice (symbol :tag "Formatter") - (repeat - (symbol :tag "Formatter"))))) - (defvar-local apheleia-formatter nil "Name of formatter to use in current buffer, a symbol or nil. If non-nil, then `apheleia-formatters' should have a matching @@ -1210,7 +1127,8 @@ changes), CALLBACK, if provided, is invoked with no arguments." (defcustom apheleia-post-format-hook nil "Normal hook run after Apheleia formats a buffer successfully." - :type 'hook) + :type 'hook + :group 'apheleia) (defcustom apheleia-inhibit-functions nil "List of functions that prevent Apheleia from turning on automatically. @@ -1220,7 +1138,8 @@ can still manually enable `apheleia-mode' in such a buffer. See also `apheleia-inhibit' for another way to accomplish a similar task." - :type '(repeat function)) + :type '(repeat function) + :group 'apheleia) ;; Handle recursive references. (defvar apheleia-mode) @@ -1282,7 +1201,8 @@ to see if it is allowed." (apheleia-mode))) (define-globalized-minor-mode apheleia-global-mode - apheleia-mode apheleia-mode-maybe) + apheleia-mode apheleia-mode-maybe + :group 'apheleia) (put 'apheleia-mode 'safe-local-variable #'booleanp)) diff --git a/apheleia-formatters.el b/apheleia-formatters.el index cf18abdd..01275027 100644 --- a/apheleia-formatters.el +++ b/apheleia-formatters.el @@ -1,4 +1,4 @@ -;;; apheleia-formatters.el --- Apheleia formatter definitions -*- lexical-binding: t -*- +;;; apheleia-formatters.el --- Apheleia formatters -*- lexical-binding: t -*- ;;; Commentary: @@ -125,7 +125,8 @@ rather than using this system." (const :tag "Name of real file used for input" file) (const :tag "Name of temporary file used for input" input) (const :tag "Name of temporary file used for output" output))) - (function :tag "Formatter function")))) + (function :tag "Formatter function"))) + :group 'apheleia) (defcustom apheleia-mode-alist '(;; php-mode has to come before cc-mode @@ -213,7 +214,8 @@ to derive from, as the list is interpreted sequentially." :value-type (choice (symbol :tag "Formatter") (repeat - (symbol :tag "Formatter"))))) + (symbol :tag "Formatter")))) + :group 'apheleia) (provide 'apheleia-formatters) diff --git a/apheleia.el b/apheleia.el index 359e50b9..4646600e 100644 --- a/apheleia.el +++ b/apheleia.el @@ -23,6 +23,8 @@ ;;; Code: +(require 'apheleia-core) + (defgroup apheleia nil "Reformat buffer without moving point." :group 'external From d1303108b7b88e785a5468107e534511c5e3b82d Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Sat, 11 Mar 2023 18:54:15 +0000 Subject: [PATCH 3/8] Add various helper functions for defining formatters --- apheleia-formatters.el | 79 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/apheleia-formatters.el b/apheleia-formatters.el index 01275027..51980ca0 100644 --- a/apheleia-formatters.el +++ b/apheleia-formatters.el @@ -2,10 +2,87 @@ ;;; Commentary: -;; Formatter definitions for `apheleia'. +;; Formatter helper functions and definitions for `apheleia'. ;;; Code: +(require 'cl-lib) + +(defun apheleia-formatters-indent (tab-flag indent-flag indent-var) + "Set flag for indentation. +Helper function for `apheleia-formatters' which allows you to supply +alternating flags based on the current buffers indent configuration. If the +buffer is indented with tabs then returns TAB-FLAG. Otherwise if INDENT-VAR +is set in the buffer return INDENT-FLAG and the value of INDENT-VAR. Use this +to easily configure the indentation level of a formatter." + (cond + (indent-tabs-mode tab-flag) + (indent-var + (when-let ((indent (and (boundp indent-var) + (symbol-value indent-var)))) + (list indent-flag (number-to-string indent)))))) + +(defcustom apheleia-formatters-respect-fill-column nil + "Whether formatters should set `fill-column' related flags." + :type 'boolean + :group 'apheleia) + +(defun apheleia-formatters-fill-column (fill-flag) + "Set flag for wrap column. +Helper function to set a flag based on `fill-column'. When `fill-column' is set +and `apheleia-formatters-respect-fill-column' return a list of FILL-FLAG and +`fill-column'." + (when (and apheleia-formatters-respect-fill-column + (bound-and-true-p fill-column)) + (list fill-flag (number-to-string fill-column)))) + +(defun apheleia-formatters-locate-file (file-flag file-name) + "Set a flag based on a dominating-file. +Look for a file up recursively from the current directory until FILE-NAME is +found. If found return a list of FILE-FLAG and the absolute path to the located +FILE-NAME." + (when-let ((file (locate-dominating-file default-directory file-name))) + (list file-flag file))) + +(defun apheleia-formatters-extension-p (&rest exts) + "Assert whether current buffer has an extension in EXTS." + (when-let ((name buffer-file-name) + (ext (file-name-extension name))) + (cl-find-if (apply-partially #'string-equal ext) + exts))) + +(defcustom apheleia-formatters-mode-extension-assoc + '((c-mode . ".c") + (c-ts-mode . ".c") + (c++-mode . ".cpp") + (c++-ts-mode . ".cpp") + (glsl-mode . ".glsl") + (java-mode . ".java") + (java-ts-mode . ".java")) + "Association list between major-modes and common file extensions for them." + :type 'alist + :group 'apheleia) + +(defun apheleia-formatters-mode-extension (&optional flag) + "Get a file-extension based on the current `major-mode'. +If FLAG is set this function returns a list of FLAG and then the extension. +Otherwise return the extension only." + (when-let ((ext + (alist-get major-mode apheleia-formatters-mode-extension-assoc))) + (if flag + (list flag ext) + ext))) + +(defun apheleia-formatters-local-buffer-file-name () + "Get variable `buffer-file-name' without any remote components." + (when-let ((name buffer-file-name)) + (let ((remote (file-remote-p name))) + (if remote + (substring name (length remote)) + name)))) + + + (defcustom apheleia-formatters '((bean-format . ("bean-format")) (black . ("black" "-")) From fb592ddd6baf8f52c7dff0df5d2a99e58dc0f9dc Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Sat, 11 Mar 2023 19:19:34 +0000 Subject: [PATCH 4/8] Add numerous formatter definitions and refactor existing ones --- apheleia-formatters.el | 84 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/apheleia-formatters.el b/apheleia-formatters.el index 51980ca0..0319d218 100644 --- a/apheleia-formatters.el +++ b/apheleia-formatters.el @@ -22,6 +22,21 @@ to easily configure the indentation level of a formatter." (symbol-value indent-var)))) (list indent-flag (number-to-string indent)))))) +(defun apheleia-formatters-js-indent (tab-flag indent-flag) + "Variant of `apheleia-formatters-indent' for JavaScript like modes. +See `apheleia-formatters-indent' for a description of TAB-FLAG and +INDENT-FLAG." + (apheleia-formatters-indent + tab-flag indent-flag + (cl-case major-mode + (json-mode 'js-indent-level) + (json-ts-mode 'json-ts-mode-indent-offset) + (js-mode 'js-indent-level) + (js-jsx-mode 'js-indent-level) + (js2-mode 'js2-basic-offset) + (js2-jsx-mode 'js2-basic-offset) + (js3-mode 'js3-indent-level)))) + (defcustom apheleia-formatters-respect-fill-column nil "Whether formatters should set `fill-column' related flags." :type 'boolean @@ -42,7 +57,7 @@ Look for a file up recursively from the current directory until FILE-NAME is found. If found return a list of FILE-FLAG and the absolute path to the located FILE-NAME." (when-let ((file (locate-dominating-file default-directory file-name))) - (list file-flag file))) + (list file-flag (concat (expand-file-name file) file-name)))) (defun apheleia-formatters-extension-p (&rest exts) "Assert whether current buffer has an extension in EXTS." @@ -84,28 +99,58 @@ Otherwise return the extension only." (defcustom apheleia-formatters - '((bean-format . ("bean-format")) - (black . ("black" "-")) + '((asmfmt . ("asmfmt")) + (astyle ("astyle" (apheleia-formatters-locate-file + "--options" ".astylerc"))) + (atsfmt . ("atsfmt")) + (beautysh . ("beautysh" + (when-let ((indent (bound-and-true-p sh-basic-offset))) + (list "--indent-size" (number-to-string indent))) + (when indent-tabs-mode "--tab") + "-")) + (bean-format . ("bean-format")) + (black . ("black" + (when (apheleia-formatters-extension-p "pyi") "--pyi") + (apheleia-formatters-fill-column "--line-length") + "-")) (brittany . ("brittany")) + (bsrefmt . ("bsrefmt")) + (buildifier . ("buildifier")) + (cabal-fmt . ("cabal-fmt")) (caddyfmt . ("caddy" "fmt" "-")) (clang-format . ("clang-format" "-assume-filename" (or (buffer-file-name) - (cdr (assoc major-mode - '((c-mode . ".c") - (c++-mode . ".cpp") - (cuda-mode . ".cu") - (protobuf-mode . ".proto")))) + (apheleia-formatters-mode-extension) ".c"))) + (cmake-format . ("cmake-format" "-")) (crystal-tool-format . ("crystal" "tool" "format" "-")) (dart-format . ("dart" "format")) + (dotnet . ("dotnet" "format")) (elm-format . ("elm-format" "--yes" "--stdin")) (fish-indent . ("fish_indent")) + (gawk . ("gawk" "-f" "-" "--pretty-print=-")) (gofmt . ("gofmt")) (gofumpt . ("gofumpt")) (goimports . ("goimports")) (google-java-format . ("google-java-format" "-")) + (html-tidy "tidy" + "--quiet" "yes" "-indent" "auto" "--vertical-space" "yes" + "--tidy-mark" "no" + (when (derived-mode-p 'nxml-mode) + "-xml") + (apheleia-formatters-indent + (list "--indent-with-tabs" "yes") + "--indent-spaces" + (cond + ((derived-mode-p 'nxml-mode) + 'nxml-child-indent) + ((derived-mode-p 'web-mode) + 'web-mode-indent-style))) + (apheleia-formatters-fill-column "-wrap")) (isort . ("isort" "-")) + (jq "jq" "." + (apheleia-formatters-js-indent "--tab" "--indent")) (lisp-indent . apheleia-indent-lisp-buffer) (ktlint . ("ktlint" "--stdin" "-F")) (latexindent . ("latexindent" "--logfile=/dev/null")) @@ -113,8 +158,11 @@ Otherwise return the extension only." (nixfmt . ("nixfmt")) (ocamlformat . ("ocamlformat" "-" "--name" filepath "--enable-outside-detected-project")) + (perltidy . ("perltidy" "--quiet" "--standard-error-output")) (phpcs . ("apheleia-phpcs")) - (prettier . (npx "prettier" "--stdin-filepath" filepath)) + (prettier . (npx "prettier" "--stdin-filepath" filepath + (apheleia-formatters-js-indent + "--use-tabs" "--tab-width"))) (prettier-css . (npx "prettier" "--stdin-filepath" filepath "--parser=css")) (prettier-html @@ -135,7 +183,23 @@ Otherwise return the extension only." . (npx "prettier" "--stdin-filepath" filepath "--parser=typescript")) (prettier-yaml . (npx "prettier" "--stdin-filepath" filepath "--parser=yaml")) - (shfmt . ("shfmt" "-i" "4")) + (rubocop . ("rubocop" "--stdin" filepath "--auto-correct" + "--stderr" "--format" "quiet" "--fail-level" "fatal")) + (rufo . ("rufo" "--filename" filepath)) + (rustfmt . ("rustfmt" "--unstable-features" "--skip-children" + "--quiet" "--emit" "stdout")) + (shfmt . ("shfmt" + "-filename" filepath + "-ln" (cl-case sh-shell + ((zsh bash) "bash") + (t "posix")) + "-i" (number-to-string + (cond + (indent-tabs-mode 0) + ((boundp 'sh-basic-offset) + sh-basic-offset) + 4)) + "-")) (stylua . ("stylua" "-")) (rustfmt . ("rustfmt" "--quiet" "--emit" "stdout")) (terraform . ("terraform" "fmt" "-"))) From 2ecdddea6c803c467706029bea42828634842377 Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Sun, 12 Mar 2023 00:17:01 +0000 Subject: [PATCH 5/8] wip: Add tests for new implementation --- apheleia-formatters.el | 7 ++----- test/formatters/apheleia-ft.el | 1 + test/formatters/installers/asmfmt.bash | 6 ++++++ test/formatters/installers/astyle.bash | 1 + test/formatters/installers/beautysh.bash | 2 ++ test/formatters/installers/buildifier.bash | 2 ++ test/formatters/installers/cmake-format.bash | 2 ++ test/formatters/installers/dotnet.bash | 6 ++++++ test/formatters/installers/gawk.bash | 1 + test/formatters/installers/html-tidy.bash | 1 + test/formatters/installers/jq.bash | 1 + test/formatters/installers/perltidy.bash | 1 + test/formatters/installers/rubocop.bash | 1 + test/formatters/installers/rufo.bash | 2 ++ 14 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 test/formatters/installers/asmfmt.bash create mode 100644 test/formatters/installers/astyle.bash create mode 100644 test/formatters/installers/beautysh.bash create mode 100644 test/formatters/installers/buildifier.bash create mode 100644 test/formatters/installers/cmake-format.bash create mode 100644 test/formatters/installers/dotnet.bash create mode 100644 test/formatters/installers/gawk.bash create mode 100644 test/formatters/installers/html-tidy.bash create mode 100644 test/formatters/installers/jq.bash create mode 100644 test/formatters/installers/perltidy.bash create mode 100644 test/formatters/installers/rubocop.bash create mode 100644 test/formatters/installers/rufo.bash diff --git a/apheleia-formatters.el b/apheleia-formatters.el index 0319d218..d44590cc 100644 --- a/apheleia-formatters.el +++ b/apheleia-formatters.el @@ -100,9 +100,8 @@ Otherwise return the extension only." (defcustom apheleia-formatters '((asmfmt . ("asmfmt")) - (astyle ("astyle" (apheleia-formatters-locate-file - "--options" ".astylerc"))) - (atsfmt . ("atsfmt")) + (astyle . ("astyle" (apheleia-formatters-locate-file + "--options" ".astylerc"))) (beautysh . ("beautysh" (when-let ((indent (bound-and-true-p sh-basic-offset))) (list "--indent-size" (number-to-string indent))) @@ -114,9 +113,7 @@ Otherwise return the extension only." (apheleia-formatters-fill-column "--line-length") "-")) (brittany . ("brittany")) - (bsrefmt . ("bsrefmt")) (buildifier . ("buildifier")) - (cabal-fmt . ("cabal-fmt")) (caddyfmt . ("caddy" "fmt" "-")) (clang-format . ("clang-format" "-assume-filename" diff --git a/test/formatters/apheleia-ft.el b/test/formatters/apheleia-ft.el index b560441b..8d56cdc5 100755 --- a/test/formatters/apheleia-ft.el +++ b/test/formatters/apheleia-ft.el @@ -275,6 +275,7 @@ environment variable, defaulting to all formatters." arg) (_ (eval arg)))) command)) + (setq command (delq nil command)) (setq command (delq 'npx command)) (setq stdout-buffer (get-buffer-create (format "*apheleia-ft-stdout-%S" formatter))) diff --git a/test/formatters/installers/asmfmt.bash b/test/formatters/installers/asmfmt.bash new file mode 100644 index 00000000..b4ca19a3 --- /dev/null +++ b/test/formatters/installers/asmfmt.bash @@ -0,0 +1,6 @@ +version=1.3.2 + +dest=$(mktemp -d) +curl -L "https://github.com/klauspost/asmfmt/releases/download/v$version/asmfmt-Linux_x86_64_$version.tar.gz" | tar -xvzC "$dest" +mv "$dest/asmfmt" /usr/local/bin/asmfmt +rm -rf "$dest" diff --git a/test/formatters/installers/astyle.bash b/test/formatters/installers/astyle.bash new file mode 100644 index 00000000..2a9cde97 --- /dev/null +++ b/test/formatters/installers/astyle.bash @@ -0,0 +1 @@ +apt-get install -y astyle diff --git a/test/formatters/installers/beautysh.bash b/test/formatters/installers/beautysh.bash new file mode 100644 index 00000000..6378c809 --- /dev/null +++ b/test/formatters/installers/beautysh.bash @@ -0,0 +1,2 @@ +apt-get install -y python3-pip +pip3 install beautysh diff --git a/test/formatters/installers/buildifier.bash b/test/formatters/installers/buildifier.bash new file mode 100644 index 00000000..b17b227d --- /dev/null +++ b/test/formatters/installers/buildifier.bash @@ -0,0 +1,2 @@ +version=6.0.1 +curl -L --output /usr/local/bin/buildifier "https://github.com/bazelbuild/buildtools/releases/download/$version/buildifier-linux-amd64" && chmod +x /usr/local/bin/buildifier diff --git a/test/formatters/installers/cmake-format.bash b/test/formatters/installers/cmake-format.bash new file mode 100644 index 00000000..7af9c251 --- /dev/null +++ b/test/formatters/installers/cmake-format.bash @@ -0,0 +1,2 @@ +apt-get install -y python3-pip +pip3 install cmakelang diff --git a/test/formatters/installers/dotnet.bash b/test/formatters/installers/dotnet.bash new file mode 100644 index 00000000..648e8948 --- /dev/null +++ b/test/formatters/installers/dotnet.bash @@ -0,0 +1,6 @@ +deb_file=$(mktemp) +curl -L --output "$deb_file" https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb +sudo dpkg -i "$deb_file" +rm "$deb_file" + +apt-get update && apt-get install -y dotnet-sdk-7.0 diff --git a/test/formatters/installers/gawk.bash b/test/formatters/installers/gawk.bash new file mode 100644 index 00000000..ff015f1f --- /dev/null +++ b/test/formatters/installers/gawk.bash @@ -0,0 +1 @@ +apt-get install -y gawk diff --git a/test/formatters/installers/html-tidy.bash b/test/formatters/installers/html-tidy.bash new file mode 100644 index 00000000..d3846250 --- /dev/null +++ b/test/formatters/installers/html-tidy.bash @@ -0,0 +1 @@ +apt-get install -y tidy diff --git a/test/formatters/installers/jq.bash b/test/formatters/installers/jq.bash new file mode 100644 index 00000000..5cc70012 --- /dev/null +++ b/test/formatters/installers/jq.bash @@ -0,0 +1 @@ +apt-get install -y jq diff --git a/test/formatters/installers/perltidy.bash b/test/formatters/installers/perltidy.bash new file mode 100644 index 00000000..e2381279 --- /dev/null +++ b/test/formatters/installers/perltidy.bash @@ -0,0 +1 @@ +apt-get install -y perltidy diff --git a/test/formatters/installers/rubocop.bash b/test/formatters/installers/rubocop.bash new file mode 100644 index 00000000..3f09299a --- /dev/null +++ b/test/formatters/installers/rubocop.bash @@ -0,0 +1 @@ +apt-get install -y rubocop diff --git a/test/formatters/installers/rufo.bash b/test/formatters/installers/rufo.bash new file mode 100644 index 00000000..0dd69d2a --- /dev/null +++ b/test/formatters/installers/rufo.bash @@ -0,0 +1,2 @@ +apt-get install -y ruby +gem install rufo From 58b6219b2802b5ed1052dae5c08cbacf447669b8 Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Sun, 12 Mar 2023 01:19:54 +0000 Subject: [PATCH 6/8] fixup! wip: Add tests for new implementation --- apheleia-formatters.el | 37 ++++----- test/formatters/apheleia-ft.el | 75 ++++++++++--------- test/formatters/samplecode/asmfmt/in.asm | 0 test/formatters/samplecode/asmfmt/out.asm | 0 test/formatters/samplecode/astyle/in.c | 1 + test/formatters/samplecode/astyle/out.c | 5 ++ test/formatters/samplecode/beautysh/in.bash | 1 + test/formatters/samplecode/beautysh/out.bash | 2 + .../formatters/samplecode/buildifier/in.bazel | 0 .../samplecode/buildifier/out.bazel | 0 .../samplecode/cmake-format/in.cmake | 0 .../samplecode/cmake-format/out.cmake | 1 + test/formatters/samplecode/dotnet/in.cs | 0 test/formatters/samplecode/dotnet/out.cs | 0 test/formatters/samplecode/gawk/in.awk | 0 test/formatters/samplecode/gawk/out.awk | 0 test/formatters/samplecode/html-tidy/in.html | 1 + test/formatters/samplecode/html-tidy/out.html | 19 +++++ test/formatters/samplecode/jq/in.json | 0 test/formatters/samplecode/jq/out.json | 0 test/formatters/samplecode/perltidy/in.pl | 0 test/formatters/samplecode/perltidy/out.pl | 0 test/formatters/samplecode/rubocop/in.rb | 1 + test/formatters/samplecode/rubocop/out.rb | 0 test/formatters/samplecode/rufo/in.rb | 1 + test/formatters/samplecode/rufo/out.rb | 0 test/formatters/samplecode/shfmt/out.bash | 2 +- 27 files changed, 94 insertions(+), 52 deletions(-) create mode 100644 test/formatters/samplecode/asmfmt/in.asm create mode 100644 test/formatters/samplecode/asmfmt/out.asm create mode 120000 test/formatters/samplecode/astyle/in.c create mode 100644 test/formatters/samplecode/astyle/out.c create mode 120000 test/formatters/samplecode/beautysh/in.bash create mode 100644 test/formatters/samplecode/beautysh/out.bash create mode 100644 test/formatters/samplecode/buildifier/in.bazel create mode 100644 test/formatters/samplecode/buildifier/out.bazel create mode 100644 test/formatters/samplecode/cmake-format/in.cmake create mode 100644 test/formatters/samplecode/cmake-format/out.cmake create mode 100644 test/formatters/samplecode/dotnet/in.cs create mode 100644 test/formatters/samplecode/dotnet/out.cs create mode 100644 test/formatters/samplecode/gawk/in.awk create mode 100644 test/formatters/samplecode/gawk/out.awk create mode 100644 test/formatters/samplecode/html-tidy/in.html create mode 100644 test/formatters/samplecode/html-tidy/out.html create mode 100644 test/formatters/samplecode/jq/in.json create mode 100644 test/formatters/samplecode/jq/out.json create mode 100644 test/formatters/samplecode/perltidy/in.pl create mode 100644 test/formatters/samplecode/perltidy/out.pl create mode 120000 test/formatters/samplecode/rubocop/in.rb create mode 100644 test/formatters/samplecode/rubocop/out.rb create mode 120000 test/formatters/samplecode/rufo/in.rb create mode 100644 test/formatters/samplecode/rufo/out.rb diff --git a/apheleia-formatters.el b/apheleia-formatters.el index d44590cc..f505b93a 100644 --- a/apheleia-formatters.el +++ b/apheleia-formatters.el @@ -123,7 +123,7 @@ Otherwise return the extension only." (cmake-format . ("cmake-format" "-")) (crystal-tool-format . ("crystal" "tool" "format" "-")) (dart-format . ("dart" "format")) - (dotnet . ("dotnet" "format")) + ;; (dotnet . ("dotnet" "format")) (elm-format . ("elm-format" "--yes" "--stdin")) (fish-indent . ("fish_indent")) (gawk . ("gawk" "-f" "-" "--pretty-print=-")) @@ -132,34 +132,37 @@ Otherwise return the extension only." (goimports . ("goimports")) (google-java-format . ("google-java-format" "-")) (html-tidy "tidy" - "--quiet" "yes" "-indent" "auto" "--vertical-space" "yes" + "--quiet" "yes" "--tidy-mark" "no" + "--vertical-space" "yes" + "-indent" (when (derived-mode-p 'nxml-mode) "-xml") (apheleia-formatters-indent - (list "--indent-with-tabs" "yes") + "--indent-with-tabs" "--indent-spaces" (cond ((derived-mode-p 'nxml-mode) 'nxml-child-indent) ((derived-mode-p 'web-mode) 'web-mode-indent-style))) - (apheleia-formatters-fill-column "-wrap")) + (apheleia-formatters-fill-column "-wrap") + ) (isort . ("isort" "-")) (jq "jq" "." (apheleia-formatters-js-indent "--tab" "--indent")) (lisp-indent . apheleia-indent-lisp-buffer) - (ktlint . ("ktlint" "--stdin" "-F")) + (ktlint . ("ktlint" "--stdin" "-F" "-")) (latexindent . ("latexindent" "--logfile=/dev/null")) (mix-format . ("mix" "format" "-")) (nixfmt . ("nixfmt")) - (ocamlformat . ("ocamlformat" "-" "--name" filepath - "--enable-outside-detected-project")) + ;; (ocamlformat . ("ocamlformat" "-" "--name" filepath + ;; "--enable-outside-detected-project")) (perltidy . ("perltidy" "--quiet" "--standard-error-output")) (phpcs . ("apheleia-phpcs")) - (prettier . (npx "prettier" "--stdin-filepath" filepath - (apheleia-formatters-js-indent - "--use-tabs" "--tab-width"))) + ;; (prettier . (npx "prettier" "--stdin-filepath" filepath + ;; (apheleia-formatters-js-indent + ;; "--use-tabs" "--tab-width"))) (prettier-css . (npx "prettier" "--stdin-filepath" filepath "--parser=css")) (prettier-html @@ -180,22 +183,22 @@ Otherwise return the extension only." . (npx "prettier" "--stdin-filepath" filepath "--parser=typescript")) (prettier-yaml . (npx "prettier" "--stdin-filepath" filepath "--parser=yaml")) - (rubocop . ("rubocop" "--stdin" filepath "--auto-correct" - "--stderr" "--format" "quiet" "--fail-level" "fatal")) - (rufo . ("rufo" "--filename" filepath)) + ;; (rubocop . ("rubocop" "--stdin" filepath "--auto-correct" + ;; "--stderr" "--format" "quiet" "--fail-level" "fatal")) + ;; (rufo . ("rufo" "--filename" filepath)) (rustfmt . ("rustfmt" "--unstable-features" "--skip-children" "--quiet" "--emit" "stdout")) (shfmt . ("shfmt" "-filename" filepath - "-ln" (cl-case sh-shell - ((zsh bash) "bash") - (t "posix")) + "-ln" (cl-case (bound-and-true-p sh-shell) + (sh "posix") + (t "bash")) "-i" (number-to-string (cond (indent-tabs-mode 0) ((boundp 'sh-basic-offset) sh-basic-offset) - 4)) + (t 4))) "-")) (stylua . ("stylua" "-")) (rustfmt . ("rustfmt" "--quiet" "--emit" "stdout")) diff --git a/test/formatters/apheleia-ft.el b/test/formatters/apheleia-ft.el index 8d56cdc5..aff1ae39 100755 --- a/test/formatters/apheleia-ft.el +++ b/test/formatters/apheleia-ft.el @@ -243,42 +243,49 @@ environment variable, defaulting to all formatters." (copy-to-buffer stdout-buffer (point-min) (point-max)))) (progn + (let ((result (apheleia--format-command command nil nil))) + (setq command (nthcdr 3 result) + in-temp-real-file (nth 0 result) + out-temp-file (nth 1 result))) + (message "%S" command) + (with-current-buffer stdout-buffer (erase-buffer)) - (mapc - (lambda (arg) - (when (memq arg '(file filepath input output inplace)) - (cl-pushnew arg syms))) - command) - (when (or (memq 'file syms) (memq 'filepath syms)) - (setq in-temp-real-file (apheleia-ft--write-temp-file - in-text extension))) - (when (or (memq 'input syms) (memq 'inplace syms)) - (setq in-temp-file (apheleia-ft--write-temp-file - in-text extension)) - (when (memq 'inplace syms) - (setq out-temp-file in-temp-file))) - (when (memq 'output syms) - (setq out-temp-file (apheleia-ft--write-temp-file - "" extension))) - (setq command - (mapcar - (lambda (arg) - (pcase arg - ((or `file `filepath) - in-temp-real-file) - ((or `input `inplace) - in-temp-file) - (`output - out-temp-file) - ((guard (stringp arg)) - arg) - (_ (eval arg)))) - command)) - (setq command (delq nil command)) - (setq command (delq 'npx command)) - (setq stdout-buffer (get-buffer-create - (format "*apheleia-ft-stdout-%S" formatter))) + ;; (mapc + ;; (lambda (arg) + ;; (when (memq arg '(file filepath input output inplace)) + ;; (cl-pushnew arg syms))) + ;; command) + ;; (when (or (memq 'file syms) (memq 'filepath syms)) + ;; (setq in-temp-real-file (apheleia-ft--write-temp-file + ;; in-text extension))) + ;; (when (or (memq 'input syms) (memq 'inplace syms)) + ;; (setq in-temp-file (apheleia-ft--write-temp-file + ;; in-text extension)) + ;; (when (memq 'inplace syms) + ;; (setq out-temp-file in-temp-file))) + ;; (when (memq 'output syms) + ;; (setq out-temp-file (apheleia-ft--write-temp-file + ;; "" extension))) + ;; (setq command (delq 'npx command)) + ;; (setq command + ;; (mapcar + ;; (lambda (arg) + ;; (pcase arg + ;; ((or `file `filepath) + ;; in-temp-real-file) + ;; ((or `input `inplace) + ;; in-temp-file) + ;; (`output + ;; out-temp-file) + ;; ((guard (stringp arg)) + ;; arg) + ;; (_ (eval arg)))) + ;; command)) + ;; (setq command (delq nil command)) + ;; (setq stdout-buffer (get-buffer-create + ;; (format "*apheleia-ft-stdout-%S" formatter))) + (setq exit-status (apply #'call-process diff --git a/test/formatters/samplecode/asmfmt/in.asm b/test/formatters/samplecode/asmfmt/in.asm new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/asmfmt/out.asm b/test/formatters/samplecode/asmfmt/out.asm new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/astyle/in.c b/test/formatters/samplecode/astyle/in.c new file mode 120000 index 00000000..4e7891c2 --- /dev/null +++ b/test/formatters/samplecode/astyle/in.c @@ -0,0 +1 @@ +../clang-format/in.c \ No newline at end of file diff --git a/test/formatters/samplecode/astyle/out.c b/test/formatters/samplecode/astyle/out.c new file mode 100644 index 00000000..bfc35f67 --- /dev/null +++ b/test/formatters/samplecode/astyle/out.c @@ -0,0 +1,5 @@ +// https://www.ioccc.org/2020/burton/prog.c +int main(int b,char**i) { + long long n=B,a=I^n,r=(a/b&a)>>4,y=atoi(*++i),_=(((a^n/b)*(y>>T)|y>>S)&r)|(a^r); + printf("%.8s\n",(char*)&_); +} diff --git a/test/formatters/samplecode/beautysh/in.bash b/test/formatters/samplecode/beautysh/in.bash new file mode 120000 index 00000000..574ddb34 --- /dev/null +++ b/test/formatters/samplecode/beautysh/in.bash @@ -0,0 +1 @@ +../shfmt/in.bash \ No newline at end of file diff --git a/test/formatters/samplecode/beautysh/out.bash b/test/formatters/samplecode/beautysh/out.bash new file mode 100644 index 00000000..cfdcce7d --- /dev/null +++ b/test/formatters/samplecode/beautysh/out.bash @@ -0,0 +1,2 @@ +function f(){ +thing;} diff --git a/test/formatters/samplecode/buildifier/in.bazel b/test/formatters/samplecode/buildifier/in.bazel new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/buildifier/out.bazel b/test/formatters/samplecode/buildifier/out.bazel new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/cmake-format/in.cmake b/test/formatters/samplecode/cmake-format/in.cmake new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/cmake-format/out.cmake b/test/formatters/samplecode/cmake-format/out.cmake new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/formatters/samplecode/cmake-format/out.cmake @@ -0,0 +1 @@ + diff --git a/test/formatters/samplecode/dotnet/in.cs b/test/formatters/samplecode/dotnet/in.cs new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/dotnet/out.cs b/test/formatters/samplecode/dotnet/out.cs new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/gawk/in.awk b/test/formatters/samplecode/gawk/in.awk new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/gawk/out.awk b/test/formatters/samplecode/gawk/out.awk new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/html-tidy/in.html b/test/formatters/samplecode/html-tidy/in.html new file mode 100644 index 00000000..3cbbf29d --- /dev/null +++ b/test/formatters/samplecode/html-tidy/in.html @@ -0,0 +1 @@ +Hello world
  • 1
  • 2
    • 3
diff --git a/test/formatters/samplecode/html-tidy/out.html b/test/formatters/samplecode/html-tidy/out.html new file mode 100644 index 00000000..59b2a4b9 --- /dev/null +++ b/test/formatters/samplecode/html-tidy/out.html @@ -0,0 +1,19 @@ + + + + Hello world + + +
    +
  • 1
  • + +
  • 2
  • + +
  • +
      +
    • 3
    • +
    +
  • +
+ + diff --git a/test/formatters/samplecode/jq/in.json b/test/formatters/samplecode/jq/in.json new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/jq/out.json b/test/formatters/samplecode/jq/out.json new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/perltidy/in.pl b/test/formatters/samplecode/perltidy/in.pl new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/perltidy/out.pl b/test/formatters/samplecode/perltidy/out.pl new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/rubocop/in.rb b/test/formatters/samplecode/rubocop/in.rb new file mode 120000 index 00000000..2881c83c --- /dev/null +++ b/test/formatters/samplecode/rubocop/in.rb @@ -0,0 +1 @@ +../prettier-ruby/in.rb \ No newline at end of file diff --git a/test/formatters/samplecode/rubocop/out.rb b/test/formatters/samplecode/rubocop/out.rb new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/rufo/in.rb b/test/formatters/samplecode/rufo/in.rb new file mode 120000 index 00000000..2881c83c --- /dev/null +++ b/test/formatters/samplecode/rufo/in.rb @@ -0,0 +1 @@ +../prettier-ruby/in.rb \ No newline at end of file diff --git a/test/formatters/samplecode/rufo/out.rb b/test/formatters/samplecode/rufo/out.rb new file mode 100644 index 00000000..e69de29b diff --git a/test/formatters/samplecode/shfmt/out.bash b/test/formatters/samplecode/shfmt/out.bash index 879aa944..a5903dad 100644 --- a/test/formatters/samplecode/shfmt/out.bash +++ b/test/formatters/samplecode/shfmt/out.bash @@ -1,3 +1,3 @@ function f() { - thing + thing } From 562f16eb594c8f93daf1123c85407c035354e67e Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Sun, 12 Mar 2023 10:04:54 +0000 Subject: [PATCH 7/8] fixup! wip: Add tests for new implementation --- apheleia-formatters.el | 47 +++++++++++-------- test/formatters/apheleia-ft.el | 41 ++-------------- test/formatters/installers/dotnet.bash | 6 --- test/formatters/installers/rubocop.bash | 4 +- test/formatters/samplecode/dotnet/in.cs | 0 test/formatters/samplecode/dotnet/out.cs | 0 .../samplecode/prettier-css/out.css | 8 ++-- .../samplecode/prettier-graphql/out.graphql | 8 ++-- .../samplecode/prettier-html/out.html | 6 +-- .../samplecode/prettier-javascript/out.js | 14 +++--- .../samplecode/prettier-json/out.json | 34 +++++++------- .../samplecode/prettier-scss/out.scss | 6 +-- .../samplecode/prettier-typescript/out.ts | 6 +-- test/formatters/samplecode/rubocop/out.rb | 15 ++++++ test/formatters/samplecode/rufo/out.rb | 14 ++++++ 15 files changed, 106 insertions(+), 103 deletions(-) delete mode 100644 test/formatters/installers/dotnet.bash delete mode 100644 test/formatters/samplecode/dotnet/in.cs delete mode 100644 test/formatters/samplecode/dotnet/out.cs diff --git a/apheleia-formatters.el b/apheleia-formatters.el index f505b93a..a4aa3082 100644 --- a/apheleia-formatters.el +++ b/apheleia-formatters.el @@ -123,7 +123,6 @@ Otherwise return the extension only." (cmake-format . ("cmake-format" "-")) (crystal-tool-format . ("crystal" "tool" "format" "-")) (dart-format . ("dart" "format")) - ;; (dotnet . ("dotnet" "format")) (elm-format . ("elm-format" "--yes" "--stdin")) (fish-indent . ("fish_indent")) (gawk . ("gawk" "-f" "-" "--pretty-print=-")) @@ -156,36 +155,46 @@ Otherwise return the extension only." (latexindent . ("latexindent" "--logfile=/dev/null")) (mix-format . ("mix" "format" "-")) (nixfmt . ("nixfmt")) - ;; (ocamlformat . ("ocamlformat" "-" "--name" filepath - ;; "--enable-outside-detected-project")) + (ocamlformat . ("ocamlformat" "-" "--name" filepath + "--enable-outside-detected-project")) (perltidy . ("perltidy" "--quiet" "--standard-error-output")) (phpcs . ("apheleia-phpcs")) - ;; (prettier . (npx "prettier" "--stdin-filepath" filepath - ;; (apheleia-formatters-js-indent - ;; "--use-tabs" "--tab-width"))) + (prettier + . (npx "prettier" "--stdin-filepath" filepath + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) (prettier-css - . (npx "prettier" "--stdin-filepath" filepath "--parser=css")) + . (npx "prettier" "--stdin-filepath" filepath "--parser=css" + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) (prettier-html - . (npx "prettier" "--stdin-filepath" filepath "--parser=html")) + . (npx "prettier" "--stdin-filepath" filepath "--parser=html" + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) (prettier-graphql - . (npx "prettier" "--stdin-filepath" filepath "--parser=graphql")) + . (npx "prettier" "--stdin-filepath" filepath "--parser=graphql" + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) (prettier-javascript - . (npx "prettier" "--stdin-filepath" filepath "--parser=babel-flow")) + . (npx "prettier" "--stdin-filepath" filepath "--parser=babel-flow" + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) (prettier-json - . (npx "prettier" "--stdin-filepath" filepath "--parser=json")) + . (npx "prettier" "--stdin-filepath" filepath "--parser=json" + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) (prettier-markdown - . (npx "prettier" "--stdin-filepath" filepath "--parser=markdown")) + . (npx "prettier" "--stdin-filepath" filepath "--parser=markdown" + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) (prettier-ruby - . (npx "prettier" "--stdin-filepath" filepath "--parser=ruby")) + . (npx "prettier" "--stdin-filepath" filepath "--parser=ruby" + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) (prettier-scss - . (npx "prettier" "--stdin-filepath" filepath "--parser=scss")) + . (npx "prettier" "--stdin-filepath" filepath "--parser=scss" + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) (prettier-typescript - . (npx "prettier" "--stdin-filepath" filepath "--parser=typescript")) + . (npx "prettier" "--stdin-filepath" filepath "--parser=typescript" + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) (prettier-yaml - . (npx "prettier" "--stdin-filepath" filepath "--parser=yaml")) - ;; (rubocop . ("rubocop" "--stdin" filepath "--auto-correct" - ;; "--stderr" "--format" "quiet" "--fail-level" "fatal")) - ;; (rufo . ("rufo" "--filename" filepath)) + . (npx "prettier" "--stdin-filepath" filepath "--parser=yaml" + (apheleia-formatters-js-indent "--use-tabs" "--tab-width"))) + (rubocop . ("rubocop" "--stdin" filepath "--auto-correct" + "--stderr" "--format" "quiet" "--fail-level" "fatal")) + (rufo . ("rufo" "--filename" filepath "--simple-exit")) (rustfmt . ("rustfmt" "--unstable-features" "--skip-children" "--quiet" "--emit" "stdout")) (shfmt . ("shfmt" diff --git a/test/formatters/apheleia-ft.el b/test/formatters/apheleia-ft.el index aff1ae39..46dfd270 100755 --- a/test/formatters/apheleia-ft.el +++ b/test/formatters/apheleia-ft.el @@ -226,8 +226,12 @@ environment variable, defaulting to all formatters." (let ((load-suffixes '(".el"))) (locate-library "apheleia")))))) exec-path))) + ;; Some formatters use the current file-name or buffer-name to interpret the + ;; type of file that is being formatted. Some may not be able to determine + ;; this from the contents of the file so we set this to force it. + (rename-buffer in-file) (setq stdout-buffer (get-buffer-create - (format "*apheleia-ft-stdout-%S" formatter))) + (format "*apheleia-ft-stdout-%S%s" formatter extension))) (with-current-buffer stdout-buffer (erase-buffer)) (if (functionp command) @@ -247,44 +251,9 @@ environment variable, defaulting to all formatters." (setq command (nthcdr 3 result) in-temp-real-file (nth 0 result) out-temp-file (nth 1 result))) - (message "%S" command) (with-current-buffer stdout-buffer (erase-buffer)) - ;; (mapc - ;; (lambda (arg) - ;; (when (memq arg '(file filepath input output inplace)) - ;; (cl-pushnew arg syms))) - ;; command) - ;; (when (or (memq 'file syms) (memq 'filepath syms)) - ;; (setq in-temp-real-file (apheleia-ft--write-temp-file - ;; in-text extension))) - ;; (when (or (memq 'input syms) (memq 'inplace syms)) - ;; (setq in-temp-file (apheleia-ft--write-temp-file - ;; in-text extension)) - ;; (when (memq 'inplace syms) - ;; (setq out-temp-file in-temp-file))) - ;; (when (memq 'output syms) - ;; (setq out-temp-file (apheleia-ft--write-temp-file - ;; "" extension))) - ;; (setq command (delq 'npx command)) - ;; (setq command - ;; (mapcar - ;; (lambda (arg) - ;; (pcase arg - ;; ((or `file `filepath) - ;; in-temp-real-file) - ;; ((or `input `inplace) - ;; in-temp-file) - ;; (`output - ;; out-temp-file) - ;; ((guard (stringp arg)) - ;; arg) - ;; (_ (eval arg)))) - ;; command)) - ;; (setq command (delq nil command)) - ;; (setq stdout-buffer (get-buffer-create - ;; (format "*apheleia-ft-stdout-%S" formatter))) (setq exit-status (apply diff --git a/test/formatters/installers/dotnet.bash b/test/formatters/installers/dotnet.bash deleted file mode 100644 index 648e8948..00000000 --- a/test/formatters/installers/dotnet.bash +++ /dev/null @@ -1,6 +0,0 @@ -deb_file=$(mktemp) -curl -L --output "$deb_file" https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -sudo dpkg -i "$deb_file" -rm "$deb_file" - -apt-get update && apt-get install -y dotnet-sdk-7.0 diff --git a/test/formatters/installers/rubocop.bash b/test/formatters/installers/rubocop.bash index 3f09299a..db8c8351 100644 --- a/test/formatters/installers/rubocop.bash +++ b/test/formatters/installers/rubocop.bash @@ -1 +1,3 @@ -apt-get install -y rubocop +apt-get install -y ruby ruby-dev gcc + +gem install rubocop diff --git a/test/formatters/samplecode/dotnet/in.cs b/test/formatters/samplecode/dotnet/in.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/test/formatters/samplecode/dotnet/out.cs b/test/formatters/samplecode/dotnet/out.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/test/formatters/samplecode/prettier-css/out.css b/test/formatters/samplecode/prettier-css/out.css index 91589966..87717b98 100644 --- a/test/formatters/samplecode/prettier-css/out.css +++ b/test/formatters/samplecode/prettier-css/out.css @@ -1,6 +1,6 @@ body { - padding-left: 11em; - font-family: Georgia, "Times New Roman", Times, serif; - color: purple; - background-color: #d8da3d; + padding-left: 11em; + font-family: Georgia, "Times New Roman", Times, serif; + color: purple; + background-color: #d8da3d; } diff --git a/test/formatters/samplecode/prettier-graphql/out.graphql b/test/formatters/samplecode/prettier-graphql/out.graphql index f406f637..89828dab 100644 --- a/test/formatters/samplecode/prettier-graphql/out.graphql +++ b/test/formatters/samplecode/prettier-graphql/out.graphql @@ -1,6 +1,6 @@ { - human(id: "1000") { - name - height(unit: FOOT) - } + human(id: "1000") { + name + height(unit: FOOT) + } } diff --git a/test/formatters/samplecode/prettier-html/out.html b/test/formatters/samplecode/prettier-html/out.html index 7702be87..08b86077 100644 --- a/test/formatters/samplecode/prettier-html/out.html +++ b/test/formatters/samplecode/prettier-html/out.html @@ -1,5 +1,5 @@

- Minify HTML and any - CSS or - JS included in your markup + Minify HTML and any + CSS or + JS included in your markup

diff --git a/test/formatters/samplecode/prettier-javascript/out.js b/test/formatters/samplecode/prettier-javascript/out.js index ec9cfe46..1486b511 100644 --- a/test/formatters/samplecode/prettier-javascript/out.js +++ b/test/formatters/samplecode/prettier-javascript/out.js @@ -1,10 +1,10 @@ function HelloWorld({ - greeting = "hello", - greeted = '"World"', - silent = false, - onMouseOver, + greeting = "hello", + greeted = '"World"', + silent = false, + onMouseOver, }) { - if (!greeting) { - return null; - } + if (!greeting) { + return null; + } } diff --git a/test/formatters/samplecode/prettier-json/out.json b/test/formatters/samplecode/prettier-json/out.json index 59bb3b44..86787f9f 100644 --- a/test/formatters/samplecode/prettier-json/out.json +++ b/test/formatters/samplecode/prettier-json/out.json @@ -1,19 +1,19 @@ { - "arrowParens": "always", - "bracketSpacing": true, - "embeddedLanguageFormatting": "auto", - "htmlWhitespaceSensitivity": "css", - "insertPragma": false, - "jsxBracketSameLine": false, - "jsxSingleQuote": false, - "printWidth": 80, - "proseWrap": "preserve", - "quoteProps": "as-needed", - "requirePragma": false, - "semi": true, - "singleQuote": false, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false, - "vueIndentScriptAndStyle": false + "arrowParens": "always", + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "printWidth": 80, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false, + "vueIndentScriptAndStyle": false } diff --git a/test/formatters/samplecode/prettier-scss/out.scss b/test/formatters/samplecode/prettier-scss/out.scss index 9ae1829c..b5dfe190 100644 --- a/test/formatters/samplecode/prettier-scss/out.scss +++ b/test/formatters/samplecode/prettier-scss/out.scss @@ -3,7 +3,7 @@ $bgcolor: lightblue; $textcolor: darkblue; $fontsize: 18px; /* Use the variables */ body { - background-color: $bgcolor; - color: $textcolor; - font-size: $fontsize; + background-color: $bgcolor; + color: $textcolor; + font-size: $fontsize; } diff --git a/test/formatters/samplecode/prettier-typescript/out.ts b/test/formatters/samplecode/prettier-typescript/out.ts index 143b5a7b..b97c21b1 100644 --- a/test/formatters/samplecode/prettier-typescript/out.ts +++ b/test/formatters/samplecode/prettier-typescript/out.ts @@ -1,6 +1,6 @@ interface GreetingSettings { - greeting: string; - duration?: number; - color?: string; + greeting: string; + duration?: number; + color?: string; } declare function greet(setting: GreetingSettings): void; diff --git a/test/formatters/samplecode/rubocop/out.rb b/test/formatters/samplecode/rubocop/out.rb index e69de29b..a677a10a 100644 --- a/test/formatters/samplecode/rubocop/out.rb +++ b/test/formatters/samplecode/rubocop/out.rb @@ -0,0 +1,15 @@ +d = [30_644_250_780, 9_003_106_878, + 30_636_278_846, 66_641_217_692, 4_501_790_980, + 67_124_603_036, 13_161_973_916, 66_606_629_920, + 30_642_677_916, 30_643_069_058]; a = [] +s = $*[0] +s.each_byte do |b| + a << ('%036b' % d[b + .chr.to_i]).scan(/\d{6}/) +end +a.transpose.each do |a| + a.join.each_byte do |i| + print i == 49 ? ($*[1] || '#') : 32.chr + end + puts +end diff --git a/test/formatters/samplecode/rufo/out.rb b/test/formatters/samplecode/rufo/out.rb index e69de29b..3d18313d 100644 --- a/test/formatters/samplecode/rufo/out.rb +++ b/test/formatters/samplecode/rufo/out.rb @@ -0,0 +1,14 @@ +d = [30644250780, 9003106878, + 30636278846, 66641217692, 4501790980, + 671_24_603036, 131_61973916, 66_606629_920, + 30642677916, 30643069058]; a, s = [], $*[0] +s.each_byte { |b| + a << ("%036b" % d[b. + chr.to_i]).scan(/\d{6}/) +} +a.transpose.each { |a| + a.join.each_byte { |i| + print i == 49 ? ($*[1] || "#") : 32.chr + } + puts +} From e174f511c68573b50fcc1858a7fbb9809078b49a Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Sun, 12 Mar 2023 10:21:37 +0000 Subject: [PATCH 8/8] fixup! wip: Add tests for new implementation --- test/formatters/samplecode/asmfmt/in.asm | 45 +++++++ test/formatters/samplecode/asmfmt/out.asm | 46 +++++++ .../formatters/samplecode/buildifier/in.bazel | 4 + .../samplecode/buildifier/out.bazel | 8 ++ .../samplecode/cmake-format/in.cmake | 99 +++++++++++++++ .../samplecode/cmake-format/out.cmake | 114 ++++++++++++++++++ test/formatters/samplecode/gawk/in.awk | 26 ++++ test/formatters/samplecode/gawk/out.awk | 18 +++ test/formatters/samplecode/jq/in.json | 1 + test/formatters/samplecode/jq/out.json | 19 +++ test/formatters/samplecode/perltidy/in.pl | 2 + test/formatters/samplecode/perltidy/out.pl | 32 +++++ 12 files changed, 414 insertions(+) mode change 100644 => 120000 test/formatters/samplecode/jq/in.json diff --git a/test/formatters/samplecode/asmfmt/in.asm b/test/formatters/samplecode/asmfmt/in.asm index e69de29b..61064ea5 100644 --- a/test/formatters/samplecode/asmfmt/in.asm +++ b/test/formatters/samplecode/asmfmt/in.asm @@ -0,0 +1,45 @@ +# Taken from https://files.klauspost.com/diff.html + +#include "zasm_GOOS_GOARCH.h" +#include "funcdata.h" +#include "textflag.h" + +TEXT runtime·rt0_go(SB),NOSPLIT,$0 + // copy arguments forward on an even stack + MOVQ DI, AX // argc + MOVQ SI, BX // argv + SUBQ $(4*8+7), SP // 2args 2auto + ANDQ $~15, SP + MOVQ AX, 16(SP) + MOVQ BX, 24(SP) + + // create istack out of the given (operating system) stack. + // _cgo_init may update stackguard. + MOVQ $runtime·g0(SB), DI + LEAQ (-64*1024+104)(SP), BX + MOVQ BX, g_stackguard0(DI) + MOVQ BX, g_stackguard1(DI) + MOVQ BX, (g_stack+stack_lo)(DI) + MOVQ SP, (g_stack+stack_hi)(DI) + + // find out information about the processor we're on + MOVQ $0, AX + CPUID + CMPQ AX, $0 + JE nocpuinfo + MOVQ $1, AX + CPUID + MOVL CX, runtime·cpuid_ecx(SB) + MOVL DX, runtime·cpuid_edx(SB) +nocpuinfo: + + // if there is an _cgo_init, call it. + MOVQ _cgo_init(SB), AX + TESTQ AX, AX + JZ needtls + // g0 already in DI + MOVQ DI, CX // Win64 uses CX for first parameter + MOVQ $setg_gcc<>(SB), SI + CALL AX + + diff --git a/test/formatters/samplecode/asmfmt/out.asm b/test/formatters/samplecode/asmfmt/out.asm index e69de29b..9425312e 100644 --- a/test/formatters/samplecode/asmfmt/out.asm +++ b/test/formatters/samplecode/asmfmt/out.asm @@ -0,0 +1,46 @@ +# Taken from https: // files.klauspost.com/diff.html + +#include "zasm_GOOS_GOARCH.h" +#include "funcdata.h" +#include "textflag.h" + +TEXT runtime·rt0_go(SB), NOSPLIT, $0 + // copy arguments forward on an even stack + MOVQ DI, AX // argc + MOVQ SI, BX // argv + SUBQ $(4*8+7), SP // 2args 2auto + ANDQ $~15, SP + MOVQ AX, 16(SP) + MOVQ BX, 24(SP) + + // create istack out of the given (operating system) stack. + // _cgo_init may update stackguard. + MOVQ $runtime·g0(SB), DI + LEAQ (-64*1024+104)(SP), BX + MOVQ BX, g_stackguard0(DI) + MOVQ BX, g_stackguard1(DI) + MOVQ BX, (g_stack+stack_lo)(DI) + MOVQ SP, (g_stack+stack_hi)(DI) + + // find out information about the processor we're on + MOVQ $0, AX + CPUID + CMPQ AX, $0 + JE nocpuinfo + MOVQ $1, AX + CPUID + MOVL CX, runtime·cpuid_ecx(SB) + MOVL DX, runtime·cpuid_edx(SB) + +nocpuinfo: + + // if there is an _cgo_init, call it. + MOVQ _cgo_init(SB), AX + TESTQ AX, AX + JZ needtls + + // g0 already in DI + MOVQ DI, CX // Win64 uses CX for first parameter + MOVQ $setg_gcc<>(SB), SI + CALL AX + diff --git a/test/formatters/samplecode/buildifier/in.bazel b/test/formatters/samplecode/buildifier/in.bazel index e69de29b..f4495039 100644 --- a/test/formatters/samplecode/buildifier/in.bazel +++ b/test/formatters/samplecode/buildifier/in.bazel @@ -0,0 +1,4 @@ +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library");cc_library( +name="hello-greet", +srcs=["hello-greet.cc"], +hdrs=["hello-greet.h"]);cc_binary(name="hello-world", srcs=["hello-world.cc"], deps=[":hello-greet", "//lib:hello-time"]) diff --git a/test/formatters/samplecode/buildifier/out.bazel b/test/formatters/samplecode/buildifier/out.bazel index e69de29b..ad8d7a72 100644 --- a/test/formatters/samplecode/buildifier/out.bazel +++ b/test/formatters/samplecode/buildifier/out.bazel @@ -0,0 +1,8 @@ +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") + +cc_library( + name = "hello-greet", + srcs = ["hello-greet.cc"], + hdrs = ["hello-greet.h"], +) +cc_binary(name = "hello-world", srcs = ["hello-world.cc"], deps = [":hello-greet", "//lib:hello-time"]) diff --git a/test/formatters/samplecode/cmake-format/in.cmake b/test/formatters/samplecode/cmake-format/in.cmake index e69de29b..7db69b75 100644 --- a/test/formatters/samplecode/cmake-format/in.cmake +++ b/test/formatters/samplecode/cmake-format/in.cmake @@ -0,0 +1,99 @@ +# The following multiple newlines should be collapsed into a single newline + + + + +cmake_minimum_required(VERSION 2.8.11) +project(cmakelang_test) + +# This multiline-comment should be reflowed +# into a single comment +# on one line + +# This comment should remain right before the command call. +# Furthermore, the command call should be formatted +# to a single line. +add_subdirectories(foo bar baz + foo2 bar2 baz2) + +# This very long command should be wrapped +set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) + +# This command should be split into one line per entry because it has a long argument list. +set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc source_g.cc source_h.cc) + +# The string in this command should not be split +set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") + +# This command has a very long argument and can't be aligned with the command +# end, so it should be moved to a new line with block indent + 1. +some_long_command_name("Some very long argument that really needs to be on the next line.") + +# This situation is similar but the argument to a KWARG needs to be on a +# newline instead. +set(CMAKE_CXX_FLAGS "-std=c++11 -Wall -Wno-sign-compare -Wno-unused-parameter -xx") + +set(HEADERS header_a.h header_b.h # This comment should + # be preserved, moreover it should be split + # across two lines. + header_c.h header_d.h) + + +# This part of the comment should +# be formatted +# but... +# cmake-format: off +# This bunny should remain untouched: +# .   _ ∩ +#   レヘヽ| | +#     (・x・) +#    c( uu} +# cmake-format: on +# while this part should +# be formatted again + +# This is a paragraph +# +# This is a second paragraph +# +# This is a third paragraph + +# This is a comment +# that should be joined but +# TODO(josh): This todo should not be joined with the previous line. +# NOTE(josh): Also this should not be joined with the todo. + +if(foo) +if(sbar) +# This comment is in-scope. +add_library(foo_bar_baz foo.cc bar.cc # this is a comment for arg2 + # this is more comment for arg2, it should be joined with the first. + baz.cc) # This comment is part of add_library + +other_command(some_long_argument some_long_argument) # this comment is very long and gets split across some lines + +other_command(some_long_argument some_long_argument some_long_argument) # this comment is even longer and wouldn't make sense to pack at the end of the command so it gets it's own lines +endif() +endif() + + +# This very long command should be broken up along keyword arguments +foo(nonkwarg_a nonkwarg_b HEADERS a.h b.h c.h d.h e.h f.h SOURCES a.cc b.cc d.cc DEPENDS foo bar baz) + +# This command uses a string with escaped quote chars +foo(some_arg some_arg "This is a \"string\" within a string") + +# This command uses an empty string +foo(some_arg some_arg "") + +# This command uses a multiline string +foo(some_arg some_arg " + This string is on multiple lines +") + +# No, I really want this to look ugly +# cmake-format: off +add_library(a b.cc + c.cc d.cc + e.cc) +# cmake-format: on diff --git a/test/formatters/samplecode/cmake-format/out.cmake b/test/formatters/samplecode/cmake-format/out.cmake index 8b137891..360b7203 100644 --- a/test/formatters/samplecode/cmake-format/out.cmake +++ b/test/formatters/samplecode/cmake-format/out.cmake @@ -1 +1,115 @@ +# The following multiple newlines should be collapsed into a single newline +cmake_minimum_required(VERSION 2.8.11) +project(cmakelang_test) + +# This multiline-comment should be reflowed into a single comment on one line + +# This comment should remain right before the command call. Furthermore, the +# command call should be formatted to a single line. +add_subdirectories(foo bar baz foo2 bar2 baz2) + +# This very long command should be wrapped +set(HEADERS very_long_header_name_a.h very_long_header_name_b.h + very_long_header_name_c.h) + +# This command should be split into one line per entry because it has a long +# argument list. +set(SOURCES + source_a.cc + source_b.cc + source_d.cc + source_e.cc + source_f.cc + source_g.cc + source_h.cc) + +# The string in this command should not be split +set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS + "-std=c++11 -Wall -Wextra") + +# This command has a very long argument and can't be aligned with the command +# end, so it should be moved to a new line with block indent + 1. +some_long_command_name( + "Some very long argument that really needs to be on the next line.") + +# This situation is similar but the argument to a KWARG needs to be on a newline +# instead. +set(CMAKE_CXX_FLAGS + "-std=c++11 -Wall -Wno-sign-compare -Wno-unused-parameter -xx") + +set(HEADERS + header_a.h header_b.h # This comment should be preserved, moreover it should + # be split across two lines. + header_c.h header_d.h) + +# This part of the comment should be formatted but... +# cmake-format: off +# This bunny should remain untouched: +# .   _ ∩ +#   レヘヽ| | +#     (・x・) +#    c( uu} +# cmake-format: on +# while this part should be formatted again + +# This is a paragraph +# +# This is a second paragraph +# +# This is a third paragraph + +# This is a comment that should be joined but +# TODO(josh): This todo should not be joined with the previous line. +# NOTE(josh): Also this should not be joined with the todo. + +if(foo) + if(sbar) + # This comment is in-scope. + add_library( + foo_bar_baz + foo.cc bar.cc # this is a comment for arg2 this is more comment for arg2, + # it should be joined with the first. + baz.cc) # This comment is part of add_library + + other_command( + some_long_argument some_long_argument) # this comment is very long and + # gets split across some lines + + other_command( + some_long_argument some_long_argument some_long_argument) # this comment + # is even longer + # and wouldn't + # make sense to + # pack at the + # end of the + # command so it + # gets it's own + # lines + endif() +endif() + +# This very long command should be broken up along keyword arguments +foo(nonkwarg_a nonkwarg_b + HEADERS a.h b.h c.h d.h e.h f.h + SOURCES a.cc b.cc d.cc + DEPENDS foo + bar baz) + +# This command uses a string with escaped quote chars +foo(some_arg some_arg "This is a \"string\" within a string") + +# This command uses an empty string +foo(some_arg some_arg "") + +# This command uses a multiline string +foo(some_arg some_arg " + This string is on multiple lines +") + +# No, I really want this to look ugly +# cmake-format: off +add_library(a b.cc + c.cc d.cc + e.cc) +# cmake-format: on diff --git a/test/formatters/samplecode/gawk/in.awk b/test/formatters/samplecode/gawk/in.awk index e69de29b..8c27da26 100644 --- a/test/formatters/samplecode/gawk/in.awk +++ b/test/formatters/samplecode/gawk/in.awk @@ -0,0 +1,26 @@ +BEGIN { + +print "Users and thier corresponding home" + +print " UserName \t HomePath" + +print "___________ \t __________" + +FS=":" + +} + +{ + + if ($2 == "foo") { + print $2 + } +print $1 " \t " $6 + +} + +END { + +print "The end" + +} diff --git a/test/formatters/samplecode/gawk/out.awk b/test/formatters/samplecode/gawk/out.awk index e69de29b..d9f2da07 100644 --- a/test/formatters/samplecode/gawk/out.awk +++ b/test/formatters/samplecode/gawk/out.awk @@ -0,0 +1,18 @@ +BEGIN { + print "Users and thier corresponding home" + print " UserName \t HomePath" + print "___________ \t __________" + FS = ":" +} + +{ + if ($2 == "foo") { + print $2 + } + print $1 " \t " $6 +} + +END { + print "The end" +} + diff --git a/test/formatters/samplecode/jq/in.json b/test/formatters/samplecode/jq/in.json deleted file mode 100644 index e69de29b..00000000 diff --git a/test/formatters/samplecode/jq/in.json b/test/formatters/samplecode/jq/in.json new file mode 120000 index 00000000..599a1e23 --- /dev/null +++ b/test/formatters/samplecode/jq/in.json @@ -0,0 +1 @@ +../prettier-json/in.json \ No newline at end of file diff --git a/test/formatters/samplecode/jq/out.json b/test/formatters/samplecode/jq/out.json index e69de29b..86787f9f 100644 --- a/test/formatters/samplecode/jq/out.json +++ b/test/formatters/samplecode/jq/out.json @@ -0,0 +1,19 @@ +{ + "arrowParens": "always", + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "printWidth": 80, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false, + "vueIndentScriptAndStyle": false +} diff --git a/test/formatters/samplecode/perltidy/in.pl b/test/formatters/samplecode/perltidy/in.pl index e69de29b..05eb420d 100644 --- a/test/formatters/samplecode/perltidy/in.pl +++ b/test/formatters/samplecode/perltidy/in.pl @@ -0,0 +1,2 @@ +# Taken from https://stackoverflow.com/q/30848816 +while(read+STDIN,$_,2048){$a=29;$b=73;$c=142;$t=255;@t=map{$_%16or$t^=$c^=($m=(11,10,116,100,11,122,20,100)[$_/16%8])&110;$t^=(72,@z=(64,72,$a^=12*($_%16-2?0:$m&17)),$b^=$_%64?12:0,@z)[$_%8]}(16..271);if((@a=unx"C*",$_)[20]&48){$h=5;$_=unxb24,join"",@b=map{xB8,unxb8,chr($_^$a[--$h+84])}@ARGV;s/...$/1$&/;$d=unxV,xb25,$_;$e=256|(ord$b[4])<<9|ord$b[3];$d=$d>>8^($f=$t&($d>>12^$d>>4^$d^$d/8))<<17,$e=$e>>8^($t&($g=($q=$e>>14&7^$e)^$q*8^$q<<6))<<9,$_=$t[$_]^(($h>>=8)+=$f+(~$g&$t))for@a[128..$#a]}print+x"C*",@a} diff --git a/test/formatters/samplecode/perltidy/out.pl b/test/formatters/samplecode/perltidy/out.pl index e69de29b..7cc9d407 100644 --- a/test/formatters/samplecode/perltidy/out.pl +++ b/test/formatters/samplecode/perltidy/out.pl @@ -0,0 +1,32 @@ +# Taken from https://stackoverflow.com/q/30848816 +while ( read +STDIN, $_, 2048 ) { + $a = 29; + $b = 73; + $c = 142; + $t = 255; + @t = map { + $_ % 16 + or $t ^= $c ^= + ( $m = ( 11, 10, 116, 100, 11, 122, 20, 100 )[ $_ / 16 % 8 ] ) & 110; + $t ^= ( + 72, + @z = ( 64, 72, $a ^= 12 * ( $_ % 16 - 2 ? 0 : $m & 17 ) ), + $b ^= $_ % 64 ? 12 : 0, @z + )[ $_ % 8 ] + } ( 16 .. 271 ); + if ( ( @a = unx "C*", $_ )[20] & 48 ) { + $h = 5; + $_ = unxb24, join "", + @b = map { xB8, unxb8, chr( $_ ^ $a[ --$h + 84 ] ) } @ARGV; + s/...$/1$&/; + $d = unxV, xb25, $_; + $e = 256 | ( ord $b[4] ) << 9 | ord $b[3]; + $d = $d >> 8 ^ ( $f = $t & ( $d >> 12 ^ $d >> 4 ^ $d ^ $d / 8 ) ) << 17, + $e = + $e + >> 8 ^ ( $t & ( $g = ( $q = $e >> 14 & 7 ^ $e ) ^ $q * 8 ^ $q << 6 ) ) + << 9, $_ = $t[$_] ^ ( ( $h >>= 8 ) += $f + ( ~$g & $t ) ) + for @a[ 128 .. $#a ]; + } + print +x "C*", @a; +}