pkryger/difftastic.el

Wrapper for difftastic

Emacs Lisp

128

315 commits

updated Sep 15, 2026

See the code

README

#+title: difftastic.el - Wrapper for difftastic
#+author: Przemysław Kryger
#+language: en
#+startup: showeverything
#+startup: literallinks
#+options: toc:nil num:nil author:nil

#+html: <a href="https://melpa.org/#/difftastic"><img alt="MELPA" src="https://melpa.org/packages/difftastic-badge.svg"/></a>
#+html: <a href="https://github.com/pkryger/difftastic.el/actions/workflows/test.yml"><img alt="CI Tests" src="https://github.com/pkryger/difftastic.el/actions/workflows/test.yml/badge.svg"/></a>
#+html: <a href="https://coveralls.io/github/pkryger/difftastic.el?branch=main"><img alt="Coveralls" src="https://coveralls.io/repos/github/pkryger/difftastic.el/badge.svg?branch=main"/></a>

The =difftastic= Emacs package is designed to integrate
[[https://github.com/wilfred/difftastic][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.

** Table of Contents                                               :noexport:
:properties:
:toc:      :include all
:end:
:contents:
- [[#features][Features]]
- [[#installation][Installation]]
  - [[#installing-from-melpa][Installing from MELPA]]
  - [[#installing-from-github][Installing from GitHub]]
  - [[#manual-installation][Manual Installation]]
- [[#configuration][Configuration]]
  - [[#manual-key-bindings-configuration][Manual Key Bindings Configuration]]
- [[#usage][Usage]]
  - [[#general-usage][General Usage]]
  - [[#specifying-difftastic-arguments][Specifying difftastic Arguments]]
  - [[#difftastic-mode-commands][difftastic-mode Commands]]
- [[#customization][Customization]]
  - [[#face-customization][Face Customization]]
  - [[#window-management][Window Management]]
  - [[#difftastic-mode-behavior][difftastic-mode Behavior]]
  - [[#process-environment][Process Environment]]
- [[#contributing][Contributing]]
  - [[#testing][Testing]]
  - [[#documentation-autoring][Documentation Autoring]]
:end:

** Features
:properties:
:custom_id: features
:end:
- 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~ (or ~M-n~) and ~p~ / ~P~ (or
  ~M-p~), as well as ~C-M-f~, ~C-M-b~, and ~C-M-SPC~ in generated diffs.
- Hide chunks and files with ~TAB~ when on a chunk/file header.  Use ~C-u TAB~
  to hide whole file.
- DWIM workflows from =magit=.
- Use difftastic do compare files and buffers (also directly from =dired=).
- Rerun =difftastic= with ~g~ to use current window width to "reflow" content
  and/or to force language change (when called with prefix).
- Use double prefix argument to specify all =difftastic= arguments.

** Installation
:properties:
:custom_id: installation
:end:
*** Installing from MELPA
:properties:
:custom_id: installing-from-melpa
:end:
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][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)
[[https://github.com/emacscollective/borg][Borg]],
[[https://github.com/progfolio/elpaca][Elpaca]],
[[https://github.com/quelpa/quelpa][Quelpa]], or
[[https://github.com/radian-software/straight.el][straight.el]].


*** Installing from GitHub
:properties:
:custom_id: installing-from-github
:end:
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=):

#+begin_src emacs-lisp :results value silent
(use-package difftastic
  :defer t
  :vc (:url "https://github.com/pkryger/difftastic.el.git"
       :rev :newest))
#+end_src

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:

#+begin_src emacs-lisp :results value silent
(use-package difftastic
  :defer t
  :vc t
  :load-path "~/src/difftastic")
#+end_src

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][Installing from MELPA]].

*** Manual Installation
:properties:
:custom_id: manual-installation
:end:
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][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:

 #+begin_src emacs-lisp :results value silent
(add-to-list 'load-path "~/src/difftastic")
(require 'difftastic)
(require 'difftastic-bindings)
 #+end_src

** Configuration
:properties:
:custom_id: configuration
:end:
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:

#+begin_src emacs-lisp :results value silent
(difftastic-bindings-mode)
#+end_src

Or, if you use =use-package=:

#+begin_src emacs-lisp :results value silent
(use-package difftastic-bindings
  :ensure difftastic ;; or nil if you prefer manual installation
  :config (difftastic-bindings-mode))
#+end_src

By default this will bind:
  - ~M-d~ to =difftastic-magit-diff= and ~M-c~ to =difftastic-magit-show= in
    =magit-diff= prefix,
  - ~M-RET~ to =difftastic-magit-show= in =magit-blame= transient prefix and in
    =magit-blame-read-only-map= keymap,
  - ~M-d~ to =difftastic-magit-diff-buffer-file= in =magit-file-dispatch=
    prefix,
  - ~M-\=~ to =difftastic-dired-diff= in =dired-mode-map=.

Please refer to =difftastic-bindings-alist= documentation to see how to change
default bindings.  You need to toggle 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
:properties:
:custom_id: manual_key_Bindings_configuration
:end:

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 partial effect similar to the one provided by
=difftastic-bindings-mode= in default configuration:

#+begin_src emacs-lisp :results value silent
(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))))
(let ((suffix '("M-RET" "Difftastic show" difftastic-magit-show)))
  (with-eval-after-load 'magit-blame
    (unless (equal (transient-parse-suffix 'magit-blame suffix)
                   (transient-get-suffix 'magit-blame "b"))
      (transient-append-suffix 'magit-blame "b" suffix))
    (keymap-set magit-blame-read-only-mode-map
                "M-RET" #'difftastic-magit-show)))
#+end_src

Or, if you use =use-package=:

#+begin_src emacs-lisp :results value silent
(use-package difftastic
  :defer t
  :init
  (use-package transient               ; to silence compiler warnings
    :autoload (transient-get-suffix
               transient-parse-suffix))

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

** Usage
:properties:
:custom_id: usage
:end:

*** General Usage
:properties:
:custom_id: general-usage
:end:

The following commands and functions are meant to help invoking =difftastic=
depending on context and desired outcome.

- =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-magit-diff-buffer-file= - show diff for the blob or file visited
  in the current buffer with =difftastic=.  When the buffer visits a blob, then
  show the respective commit.  When the buffer visits a file, then show the
  differences between ~HEAD~ and the working tree.
- =difftastic-forge-pullreq-show-diff= - show the result of =git diff
  BASE...HEAD= with =difftastic=.  When buffer is a =forge= pull request
  buffer, of point is at a pull-request, then show diff for that pull request.
  Otherwise, ask for pull request branches to compare.
- =difftastic-forge-create-pulreq-show-diff= - show diff for a new pull
  request.  This has been designed to be used in =forge-edit-post-hook=.
- =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-file-bufer= - show the result of =difft BUFFER BUFFER-FILE.
  Language is guessed based on buffer mode.  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-git-diff-range= - transform =ARGS= for difftastic and show the
  result of =git diff ARGS REV-OR-RANGE -- FILES= with =difftastic=.

*** Specifying =difftastic= Arguments
:properties:
:custom_id: specifying-difftastic-arguments
:end:

All [[#general-usage][above]] commands (and =difftastic-rerun= described
[[#difftastic-mode-commands][below]]) support specification of =difft=
arguments.  When a command is called with a double prefix argument a popup is
presented allowing to specify desired arguments.  This is in addition to a
command specific handling of a single prefix argument.

In order to aid arguments entry and provide similarity to workflows in =magit=
and =forge=, a =transient= prefix is used for the popup.  For example, some -
less commonly used - arguments are not visible in default configuration.  Type
~C-x l~ in the menu to make them visible.  Type ~C-h C-h~ for =difftastic= help
(=man difft=).  Any other =transient= commands should work as well.

When =difftastic-use-transient-arguments= is non-nil, and extra difftastic
arguments were saved in =transient= (as described in Info node
=(transient)Saving Values=), these values will be used for future =difft=
invocations.  However, when there's a need to tune these arguments for a
specific =difft= call, a difftastic command can be called with double prefix
argument to bring a popup allowing to specify arguments.  Any arguments changed
in the popup (that were not saved) will be used for the following =difft=
invocation only.

Note that in some cases arguments will take precedence over standard and
computed values, for example =--width= is one such a argument.

*** =difftastic-mode= Commands
:properties:
:custom_id: difftastic-mode-commands
:end:
When a buffer shows =difftastic= output the following commands can be
used.  Commands are followed by their default keybindings (in parenthesis).

- =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~ or ~M-n~) - move
  point to a next logical chunk or a next file respectively.
- =difftastic-previous-chunk= (~p~), =difftastic-previous-file= (~P~ or
  ~M-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
  toggle all file chunks from the header to the end of the file.  See also
  =difftastic-hide-chunk= and =difftastic-show-chunk=.
- =forward-sexp= (~C-M-f~) - move point to end of current chunk or to an end of
  next chunk when point is already at the end of the chunk.  When called with
  argument move by that many chunks.  Binding is from a default =global-map=.
- =backward-sexp= (~C-M-b~) - move point to beginning of current chunk or to a
  beginning of previous chunk when point is already at the beginning of the
  chunk.  When called with argument move by that many chunks.  Binding is from
  a default =global-map=.
- =mark-sexp= (~C-M-SPC~) - set mark and move point to end of current chunk or
  to an end of next chunk when point is already at the end of the chunk.  When
  called with argument move by that many chunks.  Binding is from a default
  =global-map=.
- =difftastic-diff-visit-file= (~RET~),
  =difftastic-diff-visit-file-other-window=,
  =difftastic-diff-visit-file-other-frame= - from a diff visit appropriate
  version of a chunk file.  This has been modeled after
  =magit-diff-visit-file=, but there are some differences, please see
  documentation for =difftastic-diff-visit-file=.
- =difftastic-diff-visit-worktree-file= (~C-RET~, ~C-j~),
  =difftastic-diff-visit-worktree-file-other-window=,
  =difftastic-diff-visit-worktree-file-other-frame= - from a diff visit
  appropriate version of a chunk file.  This has been modeled after
  =magit-diff-visit-worktree-file=, but there are some differences, please see
  documentation for =difftastic-diff-visit-worktree-file=.

** Customization
:properties:
:custom_id: customization
:end:
*** Face Customization
:properties:
:custom_id: face-customization
:end:
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:

#+begin_src emacs-lisp :results value silent
;; 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)
#+end_src

*** Window Management
:properties:
:custom_id: window-management
:end:
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.

*** =difftastic-mode= Behavior
:properties:
:custom_id: difftastic-mode-behavior
:end:
- =difftastic-visibility-indicator= - controls whether and how to show
  hidden/visible chunk/files.
- =difftastic-buttonize-urls= - controls whether to transform URLs into buttons
  in difftastic buffers.
- =difftastic-diff-visit-avoid-head-blob= - controls whether to avoid visiting
  blob of a =HEAD= revision when visiting file form a =difftastic-mode= buffer.

*** Process Environment
:properties:
:custom_id: process-environment
:end:
The =difftastic-difft-environment= variable allows users to customize the
environment in which =difft= runs.  This is particularly useful for temporary
configurations via =let=-binding.

This variable is defined as a =defvar= (not =defcustom=) since it's primarily
intended for programmatic use rather than permanent customization.

** Contributing
:properties:
:custom_id: contributing
:end:
Contributions are welcome! Feel free to submit issues and pull requests on the
[[https://github.com/pkryger/difftastic.el][GitHub repository]].

*** Testing
:properties:
:custom_id: testing
:end:
When creating a pull request make sure all tests in
[[file:test/difftastic.t.el]] are passing.  When adding a new functionality,
please strive to add tests for it as well.

This repository uses a [[https://github.com/pkryger/el-mock.el][custom fork of
=el-mock=]] which has a few more features and some slight semantic differences.
Please make sure you're using that fork before you report issues with tests.

To run tests interactively:
- open the [[file:test/difftastic.t.el]]
- type ~M-x eval-buffer <RET>~
- open the [[file:test/difftastic-bindings.t.el]]
- type ~M-x eval-buffer <RET>~
- type ~M-x ert <RET> t <RET>~

Alternatively you can use [[https://github.com/cask/cask][Cask]] to run tests
in batch mode.  There's a convenience [[file:Makefile]] with a =test= target,
so you can just type ~make test~.

It seems that byte compilation interferres with
[[https://github.com/rejeep/el-mock.el][el-mock]].  In order to get the tests
to pass you may need to:
- type ~M-x eval-buffer <RET>~ in [[file:difftastic.el]] and in
  [[file:difftastic-bindings.el]] when running test interactively with ~M-x
  <RET> ert <RET>~,
- remove all ~.elc~ files in the development directory when running tests in
  batch mode.

This repository uses [[https://coveralls.io][Coveralls]] to track test
coverage.  After a PR has been approved for a Gighub Action run, a report will
be published [[https://coveralls.io/github/pkryger/difftastic.el][Coveralls
difftastic repo]].  Please check it out if there's no outstanding relevant
lines.

You can run all checks performed by Github Actions, by typing: ~make
bytecompile lint relint checkdoc commentary test~.

*** Documentation Autoring
:properties:
:custom_id: documentation-authoring
:end:
This package uses the following packages to help with documentation authoring:

- [[https://github.com/pkryger/org-commentary.el][org-commentary.el]] (which is
  different from the one available on MELPA!) to generate and validate
  commentary section in =difftastic.el=.  Please see the package documentation
  for usage instructions.

- [[https://github.com/alphapapa/org-make-toc][org-make-toc]] to generate and
  validate table of contents in the =README.org= file.

Appropriate functions from both of these packages are added to
=after-save-hook= and =before-save-hook= respectively, when packages are
available in user's Emacs (see =dir-locals.el=).

** Acknowledgments                                                 :noexport:
:properties:
:custom_id: acknowledgments
:end:
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
[[https://tsdh.org/posts/2022-08-01-difftastic-diffing-with-magit.html][blog
entry]].

=magit-diff= keybindings and a concept of updating faces comes from a Shiv
Jha-Mathur's [[https://shivjm.blog/better-magit-diffs/][blog entry]].

This all has been strongly influenced by - a class in itself -
[[https://github.com/magit/magit][Magit]] and
[[https://github.com/magit/transient][Transient]] Emacs packages by Jonas
Bernoulli.

** Similar Packages                                                :noexport:
:properties:
:custom_id: similar-packages
:end:
*** Diff ANSI
:properties:
:custom_id: diff-ansi
:end:
There's a [[https://codeberg.org/ideasman42/emacs-diff-ansi][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                                                         :noexport:
:properties:
:custom_id: license
:end:
This package is licensed under the
[[https://www.gnu.org/licenses/gpl-3.0.en.html][GPLv3 License]].

--------------

Happy coding! If you encounter any issues or have suggestions for improvements,
please don't hesitate to reach out on the
[[https://github.com/pkryger/difftastic.el][GitHub repository]].  Your feedback
is highly appreciated.

# LocalWords: MELPA DWIM

Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.

Contributors

pkryger

304 commits

SqrtMinusOne

4 commits

emreyolcu

2 commits

lynaghk

1 commits

pkryger/difftastic.el

Wrapper for difftastic

Emacs Lisp

128

315 commits

updated Sep 15, 2026

See the code

README

#+title: difftastic.el - Wrapper for difftastic
#+author: Przemysław Kryger
#+language: en
#+startup: showeverything
#+startup: literallinks
#+options: toc:nil num:nil author:nil

#+html: <a href="https://melpa.org/#/difftastic"><img alt="MELPA" src="https://melpa.org/packages/difftastic-badge.svg"/></a>
#+html: <a href="https://github.com/pkryger/difftastic.el/actions/workflows/test.yml"><img alt="CI Tests" src="https://github.com/pkryger/difftastic.el/actions/workflows/test.yml/badge.svg"/></a>
#+html: <a href="https://coveralls.io/github/pkryger/difftastic.el?branch=main"><img alt="Coveralls" src="https://coveralls.io/repos/github/pkryger/difftastic.el/badge.svg?branch=main"/></a>

The =difftastic= Emacs package is designed to integrate
[[https://github.com/wilfred/difftastic][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.

** Table of Contents                                               :noexport:
:properties:
:toc:      :include all
:end:
:contents:
- [[#features][Features]]
- [[#installation][Installation]]
  - [[#installing-from-melpa][Installing from MELPA]]
  - [[#installing-from-github][Installing from GitHub]]
  - [[#manual-installation][Manual Installation]]
- [[#configuration][Configuration]]
  - [[#manual-key-bindings-configuration][Manual Key Bindings Configuration]]
- [[#usage][Usage]]
  - [[#general-usage][General Usage]]
  - [[#specifying-difftastic-arguments][Specifying difftastic Arguments]]
  - [[#difftastic-mode-commands][difftastic-mode Commands]]
- [[#customization][Customization]]
  - [[#face-customization][Face Customization]]
  - [[#window-management][Window Management]]
  - [[#difftastic-mode-behavior][difftastic-mode Behavior]]
  - [[#process-environment][Process Environment]]
- [[#contributing][Contributing]]
  - [[#testing][Testing]]
  - [[#documentation-autoring][Documentation Autoring]]
:end:

** Features
:properties:
:custom_id: features
:end:
- 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~ (or ~M-n~) and ~p~ / ~P~ (or
  ~M-p~), as well as ~C-M-f~, ~C-M-b~, and ~C-M-SPC~ in generated diffs.
- Hide chunks and files with ~TAB~ when on a chunk/file header.  Use ~C-u TAB~
  to hide whole file.
- DWIM workflows from =magit=.
- Use difftastic do compare files and buffers (also directly from =dired=).
- Rerun =difftastic= with ~g~ to use current window width to "reflow" content
  and/or to force language change (when called with prefix).
- Use double prefix argument to specify all =difftastic= arguments.

** Installation
:properties:
:custom_id: installation
:end:
*** Installing from MELPA
:properties:
:custom_id: installing-from-melpa
:end:
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][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)
[[https://github.com/emacscollective/borg][Borg]],
[[https://github.com/progfolio/elpaca][Elpaca]],
[[https://github.com/quelpa/quelpa][Quelpa]], or
[[https://github.com/radian-software/straight.el][straight.el]].


*** Installing from GitHub
:properties:
:custom_id: installing-from-github
:end:
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=):

#+begin_src emacs-lisp :results value silent
(use-package difftastic
  :defer t
  :vc (:url "https://github.com/pkryger/difftastic.el.git"
       :rev :newest))
#+end_src

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:

#+begin_src emacs-lisp :results value silent
(use-package difftastic
  :defer t
  :vc t
  :load-path "~/src/difftastic")
#+end_src

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][Installing from MELPA]].

*** Manual Installation
:properties:
:custom_id: manual-installation
:end:
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][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:

 #+begin_src emacs-lisp :results value silent
(add-to-list 'load-path "~/src/difftastic")
(require 'difftastic)
(require 'difftastic-bindings)
 #+end_src

** Configuration
:properties:
:custom_id: configuration
:end:
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:

#+begin_src emacs-lisp :results value silent
(difftastic-bindings-mode)
#+end_src

Or, if you use =use-package=:

#+begin_src emacs-lisp :results value silent
(use-package difftastic-bindings
  :ensure difftastic ;; or nil if you prefer manual installation
  :config (difftastic-bindings-mode))
#+end_src

By default this will bind:
  - ~M-d~ to =difftastic-magit-diff= and ~M-c~ to =difftastic-magit-show= in
    =magit-diff= prefix,
  - ~M-RET~ to =difftastic-magit-show= in =magit-blame= transient prefix and in
    =magit-blame-read-only-map= keymap,
  - ~M-d~ to =difftastic-magit-diff-buffer-file= in =magit-file-dispatch=
    prefix,
  - ~M-\=~ to =difftastic-dired-diff= in =dired-mode-map=.

Please refer to =difftastic-bindings-alist= documentation to see how to change
default bindings.  You need to toggle 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
:properties:
:custom_id: manual_key_Bindings_configuration
:end:

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 partial effect similar to the one provided by
=difftastic-bindings-mode= in default configuration:

#+begin_src emacs-lisp :results value silent
(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))))
(let ((suffix '("M-RET" "Difftastic show" difftastic-magit-show)))
  (with-eval-after-load 'magit-blame
    (unless (equal (transient-parse-suffix 'magit-blame suffix)
                   (transient-get-suffix 'magit-blame "b"))
      (transient-append-suffix 'magit-blame "b" suffix))
    (keymap-set magit-blame-read-only-mode-map
                "M-RET" #'difftastic-magit-show)))
#+end_src

Or, if you use =use-package=:

#+begin_src emacs-lisp :results value silent
(use-package difftastic
  :defer t
  :init
  (use-package transient               ; to silence compiler warnings
    :autoload (transient-get-suffix
               transient-parse-suffix))

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

** Usage
:properties:
:custom_id: usage
:end:

*** General Usage
:properties:
:custom_id: general-usage
:end:

The following commands and functions are meant to help invoking =difftastic=
depending on context and desired outcome.

- =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-magit-diff-buffer-file= - show diff for the blob or file visited
  in the current buffer with =difftastic=.  When the buffer visits a blob, then
  show the respective commit.  When the buffer visits a file, then show the
  differences between ~HEAD~ and the working tree.
- =difftastic-forge-pullreq-show-diff= - show the result of =git diff
  BASE...HEAD= with =difftastic=.  When buffer is a =forge= pull request
  buffer, of point is at a pull-request, then show diff for that pull request.
  Otherwise, ask for pull request branches to compare.
- =difftastic-forge-create-pulreq-show-diff= - show diff for a new pull
  request.  This has been designed to be used in =forge-edit-post-hook=.
- =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-file-bufer= - show the result of =difft BUFFER BUFFER-FILE.
  Language is guessed based on buffer mode.  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-git-diff-range= - transform =ARGS= for difftastic and show the
  result of =git diff ARGS REV-OR-RANGE -- FILES= with =difftastic=.

*** Specifying =difftastic= Arguments
:properties:
:custom_id: specifying-difftastic-arguments
:end:

All [[#general-usage][above]] commands (and =difftastic-rerun= described
[[#difftastic-mode-commands][below]]) support specification of =difft=
arguments.  When a command is called with a double prefix argument a popup is
presented allowing to specify desired arguments.  This is in addition to a
command specific handling of a single prefix argument.

In order to aid arguments entry and provide similarity to workflows in =magit=
and =forge=, a =transient= prefix is used for the popup.  For example, some -
less commonly used - arguments are not visible in default configuration.  Type
~C-x l~ in the menu to make them visible.  Type ~C-h C-h~ for =difftastic= help
(=man difft=).  Any other =transient= commands should work as well.

When =difftastic-use-transient-arguments= is non-nil, and extra difftastic
arguments were saved in =transient= (as described in Info node
=(transient)Saving Values=), these values will be used for future =difft=
invocations.  However, when there's a need to tune these arguments for a
specific =difft= call, a difftastic command can be called with double prefix
argument to bring a popup allowing to specify arguments.  Any arguments changed
in the popup (that were not saved) will be used for the following =difft=
invocation only.

Note that in some cases arguments will take precedence over standard and
computed values, for example =--width= is one such a argument.

*** =difftastic-mode= Commands
:properties:
:custom_id: difftastic-mode-commands
:end:
When a buffer shows =difftastic= output the following commands can be
used.  Commands are followed by their default keybindings (in parenthesis).

- =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~ or ~M-n~) - move
  point to a next logical chunk or a next file respectively.
- =difftastic-previous-chunk= (~p~), =difftastic-previous-file= (~P~ or
  ~M-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
  toggle all file chunks from the header to the end of the file.  See also
  =difftastic-hide-chunk= and =difftastic-show-chunk=.
- =forward-sexp= (~C-M-f~) - move point to end of current chunk or to an end of
  next chunk when point is already at the end of the chunk.  When called with
  argument move by that many chunks.  Binding is from a default =global-map=.
- =backward-sexp= (~C-M-b~) - move point to beginning of current chunk or to a
  beginning of previous chunk when point is already at the beginning of the
  chunk.  When called with argument move by that many chunks.  Binding is from
  a default =global-map=.
- =mark-sexp= (~C-M-SPC~) - set mark and move point to end of current chunk or
  to an end of next chunk when point is already at the end of the chunk.  When
  called with argument move by that many chunks.  Binding is from a default
  =global-map=.
- =difftastic-diff-visit-file= (~RET~),
  =difftastic-diff-visit-file-other-window=,
  =difftastic-diff-visit-file-other-frame= - from a diff visit appropriate
  version of a chunk file.  This has been modeled after
  =magit-diff-visit-file=, but there are some differences, please see
  documentation for =difftastic-diff-visit-file=.
- =difftastic-diff-visit-worktree-file= (~C-RET~, ~C-j~),
  =difftastic-diff-visit-worktree-file-other-window=,
  =difftastic-diff-visit-worktree-file-other-frame= - from a diff visit
  appropriate version of a chunk file.  This has been modeled after
  =magit-diff-visit-worktree-file=, but there are some differences, please see
  documentation for =difftastic-diff-visit-worktree-file=.

** Customization
:properties:
:custom_id: customization
:end:
*** Face Customization
:properties:
:custom_id: face-customization
:end:
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:

#+begin_src emacs-lisp :results value silent
;; 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)
#+end_src

*** Window Management
:properties:
:custom_id: window-management
:end:
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.

*** =difftastic-mode= Behavior
:properties:
:custom_id: difftastic-mode-behavior
:end:
- =difftastic-visibility-indicator= - controls whether and how to show
  hidden/visible chunk/files.
- =difftastic-buttonize-urls= - controls whether to transform URLs into buttons
  in difftastic buffers.
- =difftastic-diff-visit-avoid-head-blob= - controls whether to avoid visiting
  blob of a =HEAD= revision when visiting file form a =difftastic-mode= buffer.

*** Process Environment
:properties:
:custom_id: process-environment
:end:
The =difftastic-difft-environment= variable allows users to customize the
environment in which =difft= runs.  This is particularly useful for temporary
configurations via =let=-binding.

This variable is defined as a =defvar= (not =defcustom=) since it's primarily
intended for programmatic use rather than permanent customization.

** Contributing
:properties:
:custom_id: contributing
:end:
Contributions are welcome! Feel free to submit issues and pull requests on the
[[https://github.com/pkryger/difftastic.el][GitHub repository]].

*** Testing
:properties:
:custom_id: testing
:end:
When creating a pull request make sure all tests in
[[file:test/difftastic.t.el]] are passing.  When adding a new functionality,
please strive to add tests for it as well.

This repository uses a [[https://github.com/pkryger/el-mock.el][custom fork of
=el-mock=]] which has a few more features and some slight semantic differences.
Please make sure you're using that fork before you report issues with tests.

To run tests interactively:
- open the [[file:test/difftastic.t.el]]
- type ~M-x eval-buffer <RET>~
- open the [[file:test/difftastic-bindings.t.el]]
- type ~M-x eval-buffer <RET>~
- type ~M-x ert <RET> t <RET>~

Alternatively you can use [[https://github.com/cask/cask][Cask]] to run tests
in batch mode.  There's a convenience [[file:Makefile]] with a =test= target,
so you can just type ~make test~.

It seems that byte compilation interferres with
[[https://github.com/rejeep/el-mock.el][el-mock]].  In order to get the tests
to pass you may need to:
- type ~M-x eval-buffer <RET>~ in [[file:difftastic.el]] and in
  [[file:difftastic-bindings.el]] when running test interactively with ~M-x
  <RET> ert <RET>~,
- remove all ~.elc~ files in the development directory when running tests in
  batch mode.

This repository uses [[https://coveralls.io][Coveralls]] to track test
coverage.  After a PR has been approved for a Gighub Action run, a report will
be published [[https://coveralls.io/github/pkryger/difftastic.el][Coveralls
difftastic repo]].  Please check it out if there's no outstanding relevant
lines.

You can run all checks performed by Github Actions, by typing: ~make
bytecompile lint relint checkdoc commentary test~.

*** Documentation Autoring
:properties:
:custom_id: documentation-authoring
:end:
This package uses the following packages to help with documentation authoring:

- [[https://github.com/pkryger/org-commentary.el][org-commentary.el]] (which is
  different from the one available on MELPA!) to generate and validate
  commentary section in =difftastic.el=.  Please see the package documentation
  for usage instructions.

- [[https://github.com/alphapapa/org-make-toc][org-make-toc]] to generate and
  validate table of contents in the =README.org= file.

Appropriate functions from both of these packages are added to
=after-save-hook= and =before-save-hook= respectively, when packages are
available in user's Emacs (see =dir-locals.el=).

** Acknowledgments                                                 :noexport:
:properties:
:custom_id: acknowledgments
:end:
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
[[https://tsdh.org/posts/2022-08-01-difftastic-diffing-with-magit.html][blog
entry]].

=magit-diff= keybindings and a concept of updating faces comes from a Shiv
Jha-Mathur's [[https://shivjm.blog/better-magit-diffs/][blog entry]].

This all has been strongly influenced by - a class in itself -
[[https://github.com/magit/magit][Magit]] and
[[https://github.com/magit/transient][Transient]] Emacs packages by Jonas
Bernoulli.

** Similar Packages                                                :noexport:
:properties:
:custom_id: similar-packages
:end:
*** Diff ANSI
:properties:
:custom_id: diff-ansi
:end:
There's a [[https://codeberg.org/ideasman42/emacs-diff-ansi][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                                                         :noexport:
:properties:
:custom_id: license
:end:
This package is licensed under the
[[https://www.gnu.org/licenses/gpl-3.0.en.html][GPLv3 License]].

--------------

Happy coding! If you encounter any issues or have suggestions for improvements,
please don't hesitate to reach out on the
[[https://github.com/pkryger/difftastic.el][GitHub repository]].  Your feedback
is highly appreciated.

# LocalWords: MELPA DWIM

Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.

Contributors

pkryger

304 commits

SqrtMinusOne

4 commits

emreyolcu

2 commits

lynaghk

1 commits

Languages

Emacs Lisp

99.3%