Skip to content

pkryger/difftastic.el

Repository files navigation

difftastic.el - Wrapper for difftastic

https://melpa.org/packages/difftastic-badge.svg https://github.com/pkryger/difftastic.el/actions/workflows/test.yml/badge.svg https://coveralls.io/repos/github/pkryger/difftastic.el/badge.svg?branch=main

Description

The difftastic Emacs package is designed to integrate difftastic - a structural diff tool - into your Emacs workflow, enhancing your code review and comparison experience. This package automatically displays difftastic’s output within Emacs using faces from your user theme, ensuring consistency with your overall coding environment.

Features

  • Configure faces to your likening. By default magit-diff-* faces from your user them are used for consistent visual experience.
  • Chunks and file navigation using n / N and p / P in generated diffs.
  • DWIM workflows from magit.
  • Rerun difftastic with g to use current window width to “reflow” content and/or to force language change (when called with prefix).

Installation

Installing from MELPA

The easiest way to install and keep difftastic up-to-date is using Emacs’ built-in package manager. difftastic is available in the MELPA repository. Refer to https://melpa.org/#/getting-started for how to install a package from MELPA.

Please see Configuration section for example configuration.

You can use any of the package managers that supports installation from MELPA. It can be one of (but not limited to): one of the built-in package, use-package, or any other package manger that handles autoloads generation, for example (in alphabetical order) Borg, Elpaca, Quelpa, or straight.el.

Installing from GitHub

The preferred method is to use built-in use-package. Add the following to your Emacs configuration file (usually ~/.emacs or ~/.emacs.d/init.el):
(use-package difftastic
  :defer t
  :vc (:url "https://github.com/pkryger/difftastic.el.git"
       :rev :newest)))

Alternatively, you can do a manual checkout and install it from there, for example:

  1. Clone this repository to a directory of your choice, for example ~/src/difftastic.
  2. Add the following lines to your Emacs configuration file:
(use-package difftastic
  :defer t
  :vc t
  :load-path "~/src/difftastic")

Yet another option is to use any of the package managers that supports installation from GitHub or a an existing checkout. That could be package-vc, or any of package managers listed in Installing from MELPA.

Manual installation

Note, that this method does not generate autoloads. As a consequence it will cause the whole package and it’s dependencies (including magit) to be loaded at startup. If you want to avoid this, ensure autoloads set up on Emacs startup. See Installing from MELPA a few package managers that can generate autoloads when package is installed.

  1. Clone this repository to a directory of your choice, for example ~/src/difftastic.
  2. Add the following line to your Emacs configuration file:
    (add-to-list 'load-path "~/src/difftastic")
    (require 'difftastic)
    (require 'difftastic-bindings)
        

Configuration

This section assumes you have difftastic’s autoloads set up at Emacs startup. If you have installed difftastic using built-in package or use-package then you should be all set.

To configure difftastic commands in relevant magit prefixes and keymaps, use the following code snippet in your Emacs configuration:

(difftastic-bindings-mode)

Or, if you use use-package:

(use-package difftastic-bindings
  :ensure difftastic ;; or nil if you prefer manual installation
  :config (difftastic-bindings-mode))

This will bind D to difftastic-magit-diff and S to difftastic-magit-show in magit-diff and magit-blame transient prefixes as well as in magit-blame-read-only-map. Please refer to difftastic-bindings documentation to see how to change default bindings.

You can adjust what bindings you want to have configured by changing values of difftastic-bindings-alist, difftastic-bindings-prefixes, and difftastic-bindings-keymaps. You need to turn the difftastic-bindings-mode off and on again to apply the changes.

The difftastic-bindings=mode was designed to have minimal dependencies and be reasonably fast to load, while providing a mechanism to bind difftastic commands, such that they are available in relevant contexts.

Manual Key Bindings Configuration

If you don’t want to use mechanism delivered by difftastic-bindings-mode you can write your own configuration. As a starting point the following snippets demonstrate how to achieve roughly the same effect as difftastic-bindings-mode:

(require 'difftastic)
(require 'transient)

(let ((suffix [("D" "Difftastic diff (dwim)" difftastic-magit-diff)
               ("S" "Difftastic show" difftastic-magit-show)]))
  (with-eval-after-load 'magit-diff
    (unless (equal (transient-parse-suffix 'magit-diff suffix)
                   (transient-get-suffix 'magit-diff '(-1 -1)))
      (transient-append-suffix 'magit-diff '(-1 -1) suffix)))
  (with-eval-after-load 'magit-blame
    (unless (equal (transient-parse-suffix 'magit-blame suffix)
                   (transient-get-suffix 'magit-blame '(-1)))
      (transient-append-suffix 'magit-blame '(-1) suffix))
    (keymap-set magit-blame-read-only-mode-map
                "D" #'difftastic-magit-show)
    (keymap-set magit-blame-read-only-mode-map
                "S" #'difftastic-magit-show)))

Or, if you use use-package:

(use-package difftastic
  :defer t
  :init
  (use-package transient               ; to silence compiler warnings
    :autoload (transient-get-suffix
               transient-parse-suffix))

  (let ((suffix [("D" "Difftastic diff (dwim)" difftastic-magit-diff)
                 ("S" "Difftastic show" difftastic-magit-show)]))
    (use-package magit-blame
      :defer t :ensure magit
      :bind
      (:map magit-blame-read-only-mode-map
            ("D" . #'difftastic-magit-diff)
            ("S" . #'difftastic-magit-show))
      :config
      (unless (equal (transient-parse-suffix 'magit-blame suffix)
                     (transient-get-suffix 'magit-blame '(-1)))
        (transient-append-suffix 'magit-blame '(-1) suffix)))
    (use-package magit-diff
      :defer t :ensure magit
      :config
      (unless (equal (transient-parse-suffix 'magit-diff suffix)
                     (transient-get-suffix 'magit-diff '(-1 -1)))
        (transient-append-suffix 'magit-diff '(-1 -1) suffix)))))

Usage

The following commands are meant to help to interact with difftastic. Commands are followed by their default keybindings in difftastic-mode (in parenthesis).
  • difftastic-magit-diff - show the result of git diff ARGS -- FILES with difftastic. This is the main entry point for DWIM action, so it tries to guess revision or range.
  • difftastic-magit-show - show the result of git show ARG with difftastic. It tries to guess ARG, and ask for it when can’t. When called with prefix argument it will ask for ARG.
  • difftastic-files - show the result of difft FILE-A FILE-B. When called with prefix argument it will ask for language to use, instead of relaying on difftastic’s detection mechanism.
  • difftastic-buffers - show the result of difft BUFFER-A BUFFER-B. Language is guessed based on buffers modes. When called with prefix argument it will ask for language to use.
  • difftastic-dired-diff - same as dired-diff, but with difftastic-files instead of the built-in diff.
  • difftastic-rerun (g) - rerun difftastic for the current buffer. It runs difftastic again in the current buffer, but respects the window configuration. It uses difftastic-rerun-requested-window-width-function which, by default, returns current window width (instead of difftastic-requested-window-width-function). It will also reuse current buffer and will not call difftastic-display-buffer-function. When called with prefix argument it will ask for language to use.
  • difftastic-next-chunk (n), difftastic-next-file (N) - move point to a next logical chunk or a next file respectively.
  • difftastic-previous-chunk (p), difftastic-previous-file (P) - move point to a previous logical chunk or a previous file respectively.
  • difftastic-toggle-chunk (TAB or C-i) - toggle visibility of a chunk at point. The point has to be in a chunk header. When called with a prefix all file chunks from the header to the end of the file. See also difftastic-hide-chunk and difftastic=show-chunk.
  • difftastic-git-diff-range - transform ARGS for difftastic and show the result of git diff ARGS REV-OR-RANGE -- FILES with difftastic.

Customization

Face Customization

You can customize the appearance of difftastic output by adjusting the faces used for highlighting. To customize a faces, use the following code snippet in your configuration:
;; Customize faces used to display difftastic output.
(setq difftastic-normal-colors-vector
  (vector
   ;; use black face from `ansi-color'
   (aref ansi-color-normal-colors-vector 0)
   ;; use face for removed marker from `difftastic'
   (aref difftastic-normal-colors-vector 1)
   ;; use face for added marker from `difftastic'
   (aref difftastic-normal-colors-vector 2)
   'my-section-face
   'my-comment-face
   'my-string-face
   'my-warning-face
   ;; use white face from `ansi-color'
   (aref ansi-color-normal-colors-vector 7)))

;; Customize highlight faces
(setq difftastic-highlight-alist
  `((,(aref difftastic-normal-colors-vector 2) . my-added-highlight)
    (,(aref difftastic-normal-colors-vector 1) . my-removed-highlight)))

;; Disable highlight faces (use difftastic's default)
(setq difftastic-highlight-alist nil)

Window management

The difftastic relies on the difft command line tool to produce an output that can be displayed in an Emacs buffer window. In short: it runs the difft, converts ANSI codes into user defined colors and displays it in window. The difft can be instructed with a hint to help it produce a content that can fit into user output, by specifying a requested width. However, the latter is not always respected.

The difftastic provides a few variables to let you customize these aspects of interaction with difft:

  • difftastic-requested-window-width-function - this function is called for a first (i.e., not a rerun) call to difft. It shall return the requested width of the output. For example this can be a half of a current frame (or a window) if the output is meant to be presented side by side.
  • difftastic-rerun-requested-window-width-function - this function is called for a rerun (i.e., not a first) call to difft. It shall return requested window width of the output. For example this can be a current window width if the output is meant to fill the whole window.
  • difftastic-display-buffer-function - this function is called after a first call to difft. It is meant to select an appropriate Emacs mechanism to display the difft output.

Contributing

Contributions are welcome! Feel free to submit issues and pull requests on the GitHub repository.

Testing

When creating a pull request make sure all tests in test/difftastic.t.el are passing. When adding a new functionality, please strive to add tests for it as well.

To run tests:

README.org and Commentary authoring and exporting

The README.org file is a source of Commentary section in the difftastic.el. That is:

  • content of Commentary should be authored in the README.org file,
  • should some content in the README.org file be omitted from Commentary section, it shall be tagged with noexport tag,
  • Commentary section can be generated, verified, and saved to difftastic.el using snippets in the following subsections.

One time set up

Just run the following snippet. It will define a new export backend used to export contents of this file to difftastic.el.

(defun difftastic-org-export-commentary-remove-top-level (backend)
  "Remove top level headline from export.
BACKEND is the export back-end being used, as a symbol."
  (org-map-entries
   (lambda ()
     (when (and (eq backend 'difftastic-commentary)
                (looking-at "^* "))
       (delete-region (point)
                      (save-excursion (outline-next-heading) (point)))
       (setq org-map-continue-from (point))))))

(add-to-list 'org-export-before-parsing-functions
             #'difftastic-org-export-commentary-remove-top-level)

(defun difftastic-org-export-commentary-src-block (src-block _contents info)
  "Transcode a SRC-BLOCK element from Org to Commentary.
CONTENTS is nil.  INFO is a plist used as a communication channel."
  (org-element-normalize-string
   (org-export-format-code-default src-block info)))

(defun difftastic-org-export-commentary-final-output (contents _backend _info)
  "Transcode CONTENTS element from Org to Commentary."
  (replace-regexp-in-string
   "^;;\\'" ""
   (replace-regexp-in-string
    "^;; $" ";;"
    (replace-regexp-in-string
     "^" ";; "
     contents))))

(org-export-define-derived-backend 'difftastic-commentary 'ascii
  :translate-alist '((src-block . difftastic-org-export-commentary-src-block))
  :filters-alist
  '((:filter-final-output . difftastic-org-export-commentary-final-output)))

(defmacro with-difftastic-org-export-commentary-defaults (body)
  "Execute BODY with difftastic org export commentary defaults."
  `(let ((org-ascii-text-width 75)
         (org-ascii-global-margin 0)
         (org-ascii-inner-margin 0))
     ,body))

Validate generated Commentary content

To quickly validate generated Commentary content - which may be usefull for developing exporting mechanism, or to iterate over different documentation formats - you can use the following snippet. When flycheck is available it will create a custom checker and run it in the generated buffer. However, the checkdoc doesn’t run in org-mode buffer, so the generated content may have issues that are not highlighted while authoring. Please open the difftastic.el and check it for any new issues.

(with-difftastic-org-export-commentary-defaults
 (org-export-to-buffer 'difftastic-commentary "*Org DIFFTASTIC-COMMENTARY Export*"
   nil nil nil nil nil
   (lambda ()
     (emacs-lisp-mode)
     (when (require 'flycheck nil t)
       (flycheck-define-checker emacs-difftastic-commentary-checkdoc
         "An Emacs Lisp style checker using CheckDoc.

Adjusted for commentary checks, boosting all diagnostics to errors
and filtering header and footer ones.
The checker runs `checkdoc-current-buffer'."
         :command ("emacs" (eval flycheck-emacs-args)
                   "--eval" (eval (flycheck-sexp-to-string
                                   (flycheck-emacs-lisp-checkdoc-variables-form)))
                   "--eval" (eval flycheck-emacs-lisp-checkdoc-form)
                   "--" source)
         :error-patterns
         ((error line-start (file-name) ":" line ": " (message) line-end))
         :error-filter
         (lambda (errors)
           (cl-remove-if
            (lambda (err)
              (string-match
               (rx (or "The first line should be of the form: \";;; package --- Summary\""
                       "You should have a summary line (\";;; .* --- .*\")"
                       "You should have a section marked \";;; Commentary:\""
                       "You should have a section marked \";;; Code:\""
                       (seq "The footer should be: (provide '"
                            (one-or-more alnum)
                            ")\\n;;; "
                            (one-or-more alnum) " ends here")))
               (flycheck-error-message err)))
            errors))
         :modes (emacs-lisp-mode)
         :enabled flycheck--emacs-lisp-checkdoc-enabled-p)
       (add-to-list 'flycheck-disabled-checkers 'emacs-lisp-checkdoc)
       (setf (car (flycheck-checker-get
                   'emacs-difftastic-commentary-checkdoc 'command))
             flycheck-this-emacs-executable)
       ;; Do not clobber user configuration
       (make-local-variable 'flycheck-checkers)
       (add-to-list 'flycheck-checkers 'emacs-difftastic-commentary-checkdoc)
       (flycheck-mode)))))

Save generated Commentary content

To generate the Commentary section and save it to difftastic.el file, you can use the following snippet. This step should be performed each time this file changes, such that the Commentary content remains up to date.

Note that checkdoc doesn’t run in org-mode buffer, so the generated content may have issues that are not highlighted while authoring. Please open the difftastic.el and check it for any new issues.

(with-difftastic-org-export-commentary-defaults
 (let ((org-export-show-temporary-export-buffer nil)
       (export-buffer "*Org DIFFTASTIC-COMMENTARY Export*"))
   (org-export-to-buffer 'difftastic-commentary export-buffer)
   (with-current-buffer (find-file-noselect "difftastic.el")
     (goto-char (point-min))
     (let ((start (progn
                    (re-search-forward "^;;; Commentary:$")
                    (beginning-of-line 3)
                    (point)))
           (end (progn
                  (re-search-forward "^;;; Code:$")
                  (end-of-line 0)
                  (point))))
       (delete-region start end))
     (insert (with-current-buffer export-buffer
               (buffer-string)))
     (save-buffer))))

Acknowledgments

This package was inspired by the need for an integration of difftastic within Emacs, enhancing the code review process for developers.

This work is based on Tassilo Horn’s blog entry.

magit-diff keybindings and a concept of updating faces comes from a Shiv Jha-Mathur’s blog entry.

This all has been strongly influenced by - a class in itself - Magit and Transient Emacs packages by Jonas Bernoulli.

Similar packages

Diff ANSI

There’s a diff-ansi package available. I haven’t spent much time on it, but at a first glance it doesn’t seem that it supports difftastic out of box. Perhaps it is possible to configure it to support difftastic as a custom tool.

License

This package is licensed under the GPLv3 License.

Happy coding! If you encounter any issues or have suggestions for improvements, please don’t hesitate to reach out on the GitHub repository. Your feedback is highly appreciated.

About

Wrapper for difftastic

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •