Skip to content

About

An Emacs package that brings Diff mode's fine-grained highlighting to unified diffs in Org source blocks, with an optional three-line takeover of ordinary diff blocks.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

12 Commits

Folders and files

Repository files navigation

org-diff-src

Emacs 27.1 or later GPL-3.0-or-later TomoElpa

A Chinese translation is available at README-cn.org.

Org renders diff source blocks with Diff mode, but their fine-grained highlighting usually disappears. Org copies font-lock face properties from the temporary buffer used to fontify a block, while Diff mode stores its refinement in overlays. org-diff-src provides org-diff-src-mode, a small diff-mode derivative that records the same refinement as face text properties, so the changed words stay colored inside the Org buffer.

The result is an ordinary Org source block: it can be folded, edited, and exported, and no Ediff session is started. Diffs can come from two strings, two files, two buffers, or two Org subtrees. A three-line configuration also lets the package take over the stock display of every diff block.

images/org-diff-src-example.png

For a behavior-only specification aimed at independent implementations, see CLEAN-ROOM.org (Chinese: CLEAN-ROOM-cn.org).

Features

  • Render any unified diff as an Org source block.
  • Keep Diff mode’s refinement in the Org buffer, not only the line-level added and removed faces.
  • Highlight the changed words, and the characters within them that differ, with diff-refine-removed, diff-refine-added, and diff-refine-changed.
  • Work without setup through the built-in org-diff-src language.
  • Take over existing diff blocks with a three-line configuration.
  • Compare two Org subtrees, two files, or two buffers.
  • Copy subtrees without changing the kill ring or the system clipboard.
  • Start no Ediff session and require no package beyond Org and Diff mode.
  • Use standard completion; Consult is not required.
  • Fall back to line-level highlighting if Diff mode’s refinement internals change.

Requirements

  • Emacs 27.1 or later. The Org version bundled with Emacs 27.1 (Org 9.3) is sufficient.
  • A diff executable. GNU diffutils is recommended. Labels use --label when available and fall back to the portable -L option.

Installation

TomoElpa (recommended)

TomoElpa is the recommended way to install org-diff-src. It provides snapshots built from the latest commit and tagged releases. The archive paths have no browsable directory listing; the landing page above lists the packages, and the URLs below are the ones package.el reads. Add the snapshots archive, refresh the package index, and let use-package install and configure the package (use-package is bundled with Emacs 29 and later; on Emacs 27–28, install it from GNU ELPA first):

(require 'package)
(add-to-list 'package-archives
             '("tomoelpa-snapshots" .
               "https://blog.prayhand13013.top/tomoelpa/snapshots/") t)
(package-initialize)
(package-refresh-contents)  ; once, or after the archive changes

(require 'use-package)

(use-package org-diff-src
  :ensure t
  :pin tomoelpa-snapshots
  :config
  (org-diff-src-setup '("diff"))
  (setq org-diff-src-default-language "diff"))

The :config block is the optional diff takeover described below. Remove it to keep the package’s default org-diff-src language.

If you prefer tagged releases over snapshots, use the releases archive and pin to it instead:

(add-to-list 'package-archives
             '("tomoelpa-releases" .
               "https://blog.prayhand13013.top/tomoelpa/releases/") t)
(package-refresh-contents)

(use-package org-diff-src
  :ensure t
  :pin tomoelpa-releases
  :config
  (org-diff-src-setup '("diff"))
  (setq org-diff-src-default-language "diff"))

Without use-package, install the package once with M-x package-install RET org-diff-src RET.

use-package with :vc

On Emacs 30.1 and later, the :vc keyword of use-package can download org-diff-src directly from GitHub, without a package archive:

(use-package org-diff-src
  :vc (:url "https://github.com/TomoeMami/org-diff-src" :rev :newest)
  :config
  (org-diff-src-setup '("diff"))
  (setq org-diff-src-default-language "diff"))

:rev :newest checks out the latest commit. Without :rev, :vc defaults to the last release; this repository has no release tags, so keep :rev :newest. :vc needs git and network access. To update later, run M-x package-vc-upgrade RET org-diff-src RET.

On older Emacs versions, use TomoElpa or straight.el instead.

straight.el

(straight-use-package
 '(org-diff-src :type git :host github :repo "TomoeMami/org-diff-src"))

With use-package:

(use-package org-diff-src
  :straight (:type git :host github :repo "TomoeMami/org-diff-src")
  :commands (org-diff-src-setup
             org-diff-src-compare-headings
             org-diff-src-compare-files
             org-diff-src-compare-buffers
             org-diff-src-insert-diff-file
             org-diff-src-insert-from-kill
             org-diff-src-wrap-region
             org-diff-src-refontify-block))

Manual

Clone the repository and add it to load-path:

(add-to-list 'load-path "/path/to/org-diff-src")
(require 'org-diff-src)

Minimal configuration for diff blocks

The package does not change diff blocks by default; it adds the org-diff-src language. To make every diff block use org-diff-src-mode, and to make newly inserted blocks use the diff language, add three lines to your init file:

(require 'org-diff-src)                     ; load the package and Org
(org-diff-src-setup '("diff"))              ; diff -> org-diff-src-mode
(setq org-diff-src-default-language "diff") ; insert new blocks as diff

The second line is the actual override. Org resolves diff to diff-mode by default, which gives a diff block only the line-level added and removed faces. Mapping diff to org-diff-src-mode adds Diff mode’s refinement. The mapping applies to existing and new blocks; a block that is already displayed is refined at the next fontification, or immediately with M-x org-diff-src-refontify-block.

The third line changes only the language written by org-diff-src-insert and the comparison commands. Omit it to keep using org-diff-src as the block language; the examples in Usage assume that default.

This is an explicit opt-in: every diff block is affected, including blocks created by other packages. org-diff-src-mode derives from diff-mode, so C-c ' editing, hunk commands, navigation, and folding keep working. To restore the stock diff-mode display, remove the second line and set org-diff-src-default-language back to "org-diff-src". Only unified diffs are refined.

use-package

Do not defer loading: the mapping must exist before Org first fontifies a diff block.

(use-package org-diff-src
  :straight (:type git :host github :repo "TomoeMami/org-diff-src")
  :demand t
  :config
  (org-diff-src-setup '("diff"))
  (setq org-diff-src-default-language "diff"))

Without the diff override

The built-in org-diff-src language needs no setup:

(require 'org-diff-src)
(org-diff-src-insert
 (org-diff-src-diff-strings "one\ntwo\n" "one\nTWO\n"))

Usage

Compare two Org subtrees

Open an Org buffer and run:

M-x org-diff-src-compare-headings

Select the original heading, then the new heading. Both subtrees are copied without touching the kill ring or clipboard, normalized to level 1, compared with diff, and inserted at point as an org-diff-src source block.

For example, given:

* Original
Some text.
foo bar baz

* New
Some text.
foo BAR baz

selecting Original and then New produces:

--- original: Original
+++ new: New
@@ -1,3 +1,3 @@
-* Original
+* New
 Some text.
-foo bar baz
+foo BAR baz

The changed words (Original / New and bar / BAR) are colored with Diff mode’s refinement faces. The heading lines are part of the comparison, so a changed title appears in the diff as well.

Language names and aliases

The default language is org-diff-src. Org resolves it to org-diff-src-mode automatically, so newly inserted org-diff-src blocks need no setup.

If you have blocks written as ediff (for example from the original snippet), register the alias once:

(require 'org-diff-src)
(org-diff-src-setup)

org-diff-src-setup maps each language in org-diff-src-extra-languages (default ("ediff")) to org-diff-src-mode. Run it before Org fontifies those blocks.

To make newly inserted blocks use ediff as well:

(setq org-diff-src-default-language "ediff")

You can register more aliases explicitly:

(org-diff-src-setup '("ediff" "diff"))

To take over diff blocks, see Minimal configuration.

Render a diff you already have

  • Copy a diff (for example from Magit) and run M-x org-diff-src-insert-from-kill.
  • Select the diff text and run M-x org-diff-src-wrap-region to replace the region with a fontified block.
  • Run M-x org-diff-src-insert-diff-file to insert the contents of a diff file.

Compare files or buffers

  • M-x org-diff-src-compare-files prompts for an original file and a new file.
  • M-x org-diff-src-compare-buffers prompts for an original buffer and a new buffer.

Both commands insert the result at point in the current Org buffer.

Refontify a block

Org normally refontifies source blocks after edits. If Lisp code changes a block without triggering fontification, run:

M-x org-diff-src-refontify-block

Commands

CommandPurpose
org-diff-src-setupRegister language aliases such as ediff.
org-diff-src-compare-headingsCompare two subtrees in the current Org buffer.
org-diff-src-compare-filesCompare two files.
org-diff-src-compare-buffersCompare two buffers.
org-diff-src-insert-diff-fileInsert a diff file as a source block.
org-diff-src-insert-from-killInsert the current kill as a source block.
org-diff-src-wrap-regionReplace the region with a source block.
org-diff-src-refontify-blockRefontify the block at point.

Lisp API

The diff-generation and insertion functions are independent of the interactive commands, so they can be used from other packages.

(org-diff-src-insert DIFF &optional LANGUAGE)
Insert DIFF as a fontified source block at point. LANGUAGE defaults to org-diff-src-default-language and must map to org-diff-src-mode; the built-in org-diff-src language does so automatically. Return the position after the block. If DIFF is nil or empty, insert org-diff-src-no-diff-message.
(org-diff-src-setup &optional LANGUAGES)
Register LANGUAGES (default org-diff-src-extra-languages) for org-diff-src-mode. Return the list of registered languages.
(org-diff-src-diff-strings OLD NEW &optional OLD-LABEL NEW-LABEL CODING)
Return a unified diff between the strings OLD and NEW.
(org-diff-src-diff-files OLD-FILE NEW-FILE &optional OLD-LABEL NEW-LABEL)
Return a unified diff between two files. Remote files are copied locally first.
(org-diff-src-diff-buffers OLD-BUFFER NEW-BUFFER &optional OLD-LABEL NEW-LABEL)
Return a unified diff between the entire contents of two buffers.
org-diff-src-mode
The major mode used for the block. It derives from diff-mode, sets diff-refine to font-lock, and records refinement as text properties.

Example:

(with-current-buffer (get-buffer-create "*diff-demo*")
  (org-mode)
  (org-diff-src-insert
   (org-diff-src-diff-strings
    "The quick brown fox\n"
    "The quick red fox\n"
    "before" "after")))

To compare two buffers programmatically:

(org-diff-src-insert
 (org-diff-src-diff-buffers (get-buffer "notes-a")
                            (get-buffer "notes-b")
                            "before" "after"))

The default source language is org-diff-src; Org resolves it to org-diff-src-mode through org-src-get-lang-mode, so no entry in org-src-lang-modes is needed. Aliases registered with org-diff-src-setup are resolved the same way.

How it works

Org copies font-lock face properties, but not overlays, from the temporary buffer it uses to fontify a source block. Diff mode implements its refinement with overlays, so the refinement is normally lost when a diff is shown inside Org.

org-diff-src-mode works around this:

  1. It derives from diff-mode.
  2. It sets diff-refine to font-lock so that refinement is computed by font-lock.
  3. It replaces the use of diff--font-lock-refined in diff-font-lock-keywords with a function that copies the refinement overlay faces into face text properties.
  4. Org’s org-src-font-lock-fontify-block can then copy those properties into the Org buffer.

If a future Emacs version removes the Diff mode refinement internals, org-diff-src-mode keeps the normal line-level highlighting instead of failing.

This is why the default block language is org-diff-src rather than diff. Org resolves diff to plain diff-mode unless you register it as an alias; see the minimal configuration.

Customization

org-diff-src-diff-switches
Switches passed to diff-command. The default is ("-u"). If you add switches, keep -u in the list because the refinement code expects a unified diff.
org-diff-src-no-diff-message
Text inserted when the diff is empty. The default is # No differences.
org-diff-src-default-language
Language used for inserted blocks. The default is "org-diff-src".
org-diff-src-extra-languages
Languages registered by org-diff-src-setup. The default is ("ediff").
org-diff-src-fontify-immediately
Whether to fontify newly inserted blocks immediately. The default is t; turn it off for very large diffs.
Faces
The refinement uses diff-refine-removed, diff-refine-added, and diff-refine-changed. Customize them with M-x customize-face or in custom-set-faces.

The mode sets diff-refine buffer-locally. You do not need to change the global value of diff-refine. Run M-x customize-group RET org-diff-src RET to see all options.

Limitations

  • Only unified diffs are refined. Context diffs and normal diffs are displayed as plain text.
  • The refinement colors are Emacs display properties. HTML or PDF export of the Org file shows the diff text, but not necessarily the refinement colors.
  • Very large diffs can be slow to fontify because refinement is computed per changed hunk and word.
  • Labels for the two diff sides are reduced to one line.
  • If a future Emacs removes the Diff mode refinement internals, the package falls back to line-level highlighting.
  • org-diff-src-compare-headings compares whole subtrees, including the heading lines. Use org-diff-src-diff-strings if you want to compare only selected body text.
  • This package renders static diffs. It is not a replacement for Ediff or Magit when you need interactive merging.

Support and Contributing

Bug reports, feature requests, and patches are welcome at the issue tracker.

Before sending a change, run:

make compile
make test
make checkdoc
make package-lint

The ERT suite is in test/org-diff-src-test.el.

Development

The package is a single file, org-diff-src.el. It is distributed through TomoElpa; the recipe and build configuration live in the tomoelpa repository.

Adding another diff source

A producer is simply a function that returns a unified diff string. For example:

(defun my/org-diff-src-from-git (rev-a rev-b)
  "Return a unified diff between REV-A and REV-B."
  (with-temp-buffer
    (let ((status (call-process "git" nil t nil
                                "diff" "-u" (concat rev-a ".." rev-b))))
      (unless (memq status '(0 1))
        (error "git diff failed: %s" (buffer-string))))
    (buffer-string)))

(org-diff-src-insert (my/org-diff-src-from-git "HEAD~1" "HEAD"))

The rendering layer only cares about the unified diff text, so new producers do not need to touch org-diff-src-mode.

Clean-room specification

A behavior-only specification for independent implementations is in CLEAN-ROOM.org (Chinese: CLEAN-ROOM-cn.org). It is written for implementers who do not want to read this implementation.

License

GPL-3.0-or-later. See LICENSE.

The original implementation was a personal Org configuration snippet for inserting Ediff-like diffs; this package generalizes its diff rendering and insertion layer.

About

An Emacs package that brings Diff mode's fine-grained highlighting to unified diffs in Org source blocks, with an optional three-line takeover of ordinary diff blocks.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages