Fancy Joiner: Join/split lines like a pro in Bash, Org, and more (Emacs package)

Below you find the latest version of (1) the package's README and (2) its main source file.

For the git repository and issues tracker, see the project's page on sr.ht.

For more packages, see Software.


README.org

(Note: if you're seeing this as a local README.org instead of online as HTML, the animated gifs may not be rendered properly, and the images may be altogether missing.)

Overview

Fancy Joiner is an Emacs package that helps you join (and split) lines of text or code in intelligent ways.

join-forward-split-backward

Figure 1: Fancy-joining forward, then fancy-splitting backward (in Bash)

It’s meant for interactive use, so it’s a good idea to bind some keys. Six commands, six keys — I use them all. I explain them below.

They behave differently when you are in shell-script-mode (alias: sh-mode).

Direction Joining Absorbing Splitting
Forward fancy-joiner-join-forward fancy-joiner-absorb-forward fancy-joiner-split-forward
Backward fancy-joiner-join-backward fancy-joiner-absorb-backward fancy-joiner-split-backward

Joining

join-forward

Figure 2: Fancy-joining forward

join-backward

Figure 3: Fancy-joining backward

Keybindings that I use: M-S-j and M-j for backward and forward, respectively.
("M" is the Alt key)

In other modes

Joins current line with previous (or next) line, removing spaces.

This is essentially the same as Emacs’ join-line function, which is extremely useful. It nukes the newline and every space between the end of the previous line and this one (or between the end of this line and the next).

In shell-script-mode

The same — but adds semicolons as needed, so that commands are properly separated. You can then transform blocks of text into one-liners, or at least condense part of your code without manually adding semicolons.

Let me show you an exaggerated example. Suppose you have these lines of Bash:

fun() {
    ((42<21)) || {
        echo "Fancy Joiner is great!"
        [[ "A" == "A" ]] &&
            echo "use-package fancy-joiner"
        case "$#" in
            0) echo Hello ;;
            *) echo World
        esac
        echo "That’s it."
    }
    if ((4>2))
    then echo Hello
    elif ((40<2))
    then echo World
    else :
    fi
    echo things &
    echo more things;
    echo less things
    for num in 10 100 1000
    do echo "${#num}"
    done
    [[ "Fancy Joiner is great" ]] && {
        (seq 3
         echo "..."
         echo "Get Fancy Joiner") |
            sed '$ s/$/ right now!/'
    } || {
        echo "Let’s check some pipes"
        echo things \
            | tr -d s \
            | sed 's/$/y/'
        echo "Let’s check some more pipes"
        echo things |
            tr -d s |
            sed 's/$/y/'
    }
}

and that you’d like to fit it all in one line. If you go to the first line of the function and keep pressed the key for fancy-joiner-join-forward, you’ll quickly see it turn into a single line.

(Since in one line this would flow to the right of the screen when exported to HTML, some strategic line breaks were added below at some points.)

fun() {
    ((42<21)) || { echo "Fancy Joiner is great!"
                   [[ "A" == "A" ]] && echo "use-package fancy-joiner"
                   case "$#" in 0) echo Hello ;; *) echo World; esac
                   echo "That’s it." ;}
    if ((4>2)); then echo Hello; elif ((40<2)); then echo World; else :; fi
    echo things & echo more things; echo less things
    for num in 10 100 1000; do echo "${#num}"; done;
    [[ "Fancy Joiner is great" ]] && { (seq 3; echo "..."; echo "Get it") |
                                           sed '$ s/$/ right now!/' ;} || {
        echo "Let’s check some pipes"; echo things | tr -d s | sed 's/$/y/'
        echo "Let’s check some more pipes"; echo things | tr -d s |
            sed 's/$/y/' ;} ;}

Hate this unreadable nest of echos and want everything spread out? That’s what fancy-joiner-split does.

Splitting

split-forward

Figure 4: Fancy-splitting forward

split-backward

Figure 5: Fancy-splitting backward

Keybindings that I use: C-S-j and C-j for backward and forward, respectively.
("C" is the Ctrl key)

In other modes

Not implemented.

In shell-script-mode

The opposite of joining.

Note that it relies on the great aggressive-indent-mode (by Artur Malabarba) to make sure that the second part of the split line is automatically indented. So do have that on. (I find it unthinkable not to use it.)

You can choose whether to split pipes like this:

echo things |
    tr -d s |
    sed 's/$/y/'

or like this:

echo things \
    | tr -d s \
    | sed 's/$/y/'

Although the latter seems more popular, I personally prefer the former, and believe that the latter’s popularity is mostly due to people not knowing that pipes can be left alone just fine at the end of the line.

But you can choose.

Absorbing

absorb-forward

Figure 6: Fancy-absorbing forward

absorb-backward

Figure 7: Fancy-absorbing backward

Keybindings that I use: s-S-j and s-j for backward and forward, respectively.
("s" is the Super key — shown as a Windows logo in some keyboards)

In all modes

What it does is join the previous (or next) line while removing dashes, asterisks, pluses, and other markers present at the beginning of the second line if they were followed by at least a space.

This is handy to quickly merge sequential org headings, items in org lists, comment characters (; #), etc.

Running it repeatedly on these lines:

* This
* is
* an
* org
* heading,
# and
# these
# are
;; commented
;; lines,
- and
  - also
  - lists.

gives you this:

* This is an org heading,
# and these are
;; commented lines,
- and also lists.

which could be continued to just this:

* This is an org heading, and these are commented lines, and also lists.

Note: in sh-mode, when in commented lines, fancy-joiner-join behaves like fancy-joiner-absorb.

Installation

See my page Software for the most up-to-date instructions on how to download and install any of my Emacs packages.

Having downloaded and installed the package and its dependencies, adapt the configurations below to your init.el file.

(use-package fancy-joiner
  :commands (global-fancy-joiner-non-sh-mode
             global-fancy-joiner-sh-mode
             fancy-joiner-non-sh-mode
             fancy-joiner-sh-mode
             fancy-joiner-see-news
             fancy-joiner-see-readme)
  :config
  (global-fancy-joiner-non-sh-mode)
  (global-fancy-joiner-sh-mode))

Or you can keybind them directly as you wish.
Example using the default keybindings from the fancy-joiner modes:

(use-package fancy-joiner
  :bind* (([(meta     shift ?j)] . fancy-joiner-join-backward)
          ([(meta           ?j)] . fancy-joiner-join-forward)
          ([(super    shift ?j)] . fancy-joiner-absorb-backward)
          ([(super          ?j)] . fancy-joiner-absorb-forward)))

(use-package sh-script
  :bind  (:map sh-mode-map
          ([(control  shift ?j)] . fancy-joiner-split-backward)
          ([(control        ?j)] . fancy-joiner-split-forward)))

We have here:

Key Action Notes
meta join in Bash, also contextually auto-detects “absorb”
super absorb  
control split  
shifted backwards  

Alternatively, if you don’t have ‘use-package’:

(require 'fancy-joiner)
(global-fancy-joiner-non-sh-mode)
(global-fancy-joiner-sh-mode)

Limitations

Not many, apparently. But these have been noticed:

  1. It doesn’t detect the boundaries of quoted texts. Therefore, it could inadvertently join or split lines in there. So use with caution around quoted text, especially if it contains the characters [;&|{] or a sentence containing "case ... in".
  2. If you run it mixing commented and non-commented lines, it will absorb the comments as if it were code. So use with caution around comments.
  3. A few other minor points brought up in the TODO.org.

Contributing

See my page Software for information about how to contribute to any of my Emacs packages.

News

1.0

Fancy Joiner Breaking News

To help preserve separation of namespaces, the prefixes of Fancy Joiner's functions and variables have all changed from ‘fj-’ to ‘fancy-joiner-’.

There's nothing to do if:

  • this is your first install of Fancy Joiner or
  • you haven't customized any variables, nor bound keys

If, however, you have added anywhere (for example, in your init.el) settings that use the old prefix, simply update them to use the new prefix. Just replace ‘fj-’ with ‘fancy-joiner-’.

You may also want to have a look at customizations you may have made in Fancy Joiner group. Use either of:

  • M-x customize-group RET fancy-joiner RET
  • (customize-group "fancy-joiner") ;<--C-x C-e

Note: The old symbols have for now been marked as obsolete and aliased to the new ones to avoid any sudden breakage. However, these aliases may be removed without further notice. So if any of the adjustments above apply to you, better do them already, before new releases.

New commands brought by this version
  • fancy-joiner-see-news ()
  • fancy-joiner-see-readme (&optional heading narrow)

License

This project follows the REUSE Specification (FAQ), which in turn is built upon SPDX.

Therefore, license and copyright information can be found in:

  • each file's comment header, or
  • an adjacent file of the same name with the additional extension .license, or
  • the .reuse/dep5 file

The full text of the licenses can be found in the LICENSES subdirectory.


fancy-joiner.el

Structure

;;; fancy-joiner.el --- Join/split lines like a pro in Bash, Org, and more -*- lexical-binding: t -*-
;;; Commentary:
;;;; For all the details, please do see the README
;;; Acknowledgments:
;;; Code:
;;;; Libraries
;;;; Obsolete symbols
;;;; Package metadata
;;;; Customizable variables
;;;; Commands
;;;;; Minor modes
;;;;;; non-sh
;;;;;; sh
;;;;; Joining, absorbing, and splitting
;;;;;; Main
;;;;;; Meta
;;;;;; Other
;;;;;; Helper
;;;;; See README
;;;;; See News
;;;; Wrapping up
;;; fancy-joiner.el ends here

Contents

;;; fancy-joiner.el --- Join/split lines like a pro in Bash, Org, and more -*- lexical-binding: t -*-

;; SPDX-FileCopyrightText:  © flandrew <https://keyoxide.org/191F5B3E212EF1E515C19918AF32B9A5C1CCCB2D>
;; SPDX-License-Identifier: GPL-3.0-or-later

;;---------------------------------------------------------------------------
;; Author:            flandrew
;; Created:           2021-06-14
;; Version:           1.0
;; Homepage:          <https://flandrew.srht.site/listful/software.html>
;; Keywords:          convenience, tools, editing, outlines
;; Package-Requires:  ((emacs "25.1"))
;;---------------------------------------------------------------------------

;; This file is part of Fancy Joiner, which is NOT part of GNU Emacs.

;; This program is free software: you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by the Free
;; Software Foundation, either version 3 of the License, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE. For more details, see the full license at
;; either LICENSES/GPL-3.0-or-later.txt or <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Fancy Joiner helps you join (and split) lines of text or code in
;; intelligent ways. It's particularly well-suited for use in shell-script
;; and org modes.
;;
;;;; For all the details, please do see the README
;;
;; Open it easily with any of the below:
;;   (find-file-read-only              "README.org")   <--- C-x C-e here¹, or
;;   (find-file-read-only-other-frame  "README.org")   <--- C-x C-e here¹, or
;;   (find-file-read-only-other-window "README.org")   <--- C-x C-e here¹
;;
;; or from any buffer:
;;   M-x fancy-joiner-see-readme
;;
;; or read it online:
;;   <https://flandrew.srht.site/listful/sw-emacs-fancy-joiner.html>
;;
;; ¹ or the key that ‘eval-last-sexp’ is bound to, if not C-x C-e.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; Acknowledgments:
;;
;; Function ‘fancy-joiner-zap-up-to-space’ was adapted from misc.el's
;; zap-up-to-char’.
;;
;; About misc.el:
;;   SPDX-FileCopyrightText:  © Free Software Foundation, Inc.
;;   SPDX-License-Identifier: GPL-3.0-or-later
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;;; Code:
;;;; Libraries

(require 'lisp-mnt)  ; lm-summary’, ‘lm-version’, ‘lm-homepage

;;;; Obsolete symbols

(let ((new 'fancy-joiner)
      (old 'fj)
      (ver "1.0"))
  (mapc (lambda (sym) (define-obsolete-variable-alias
                   (intern (format "%s-%s" old sym))
                   (intern (format "%s-%s" new sym)) ver))
        '(leave-pipes-at-eol
          aggressive-indent-when-split-backward-in-sh-mode
          aggressive-indent-when-split-forward-in-sh-mode))
  (mapc (lambda (sym) (define-obsolete-function-alias
                   (intern (format "%s-%s" old sym))
                   (intern (format "%s-%s" new sym)) ver))
        '(zap-up-to-space
          join-forward    join-backward    join
          split-forward   split-backward   split
          absorb-forward  absorb-backward)))

;;;; Package metadata

(defconst fancy-joiner--name "Fancy Joiner")

(defconst fancy-joiner--dot-el
  (format "%s.el" (file-name-sans-extension (eval-and-compile
                                              (or load-file-name
                                                  buffer-file-name)))))
(defconst fancy-joiner--readme-org
  (expand-file-name "README.org" (file-name-directory fancy-joiner--dot-el)))

(defconst fancy-joiner--summary  (lm-summary  fancy-joiner--dot-el))
(defconst fancy-joiner--version  (lm-version  fancy-joiner--dot-el))
(defconst fancy-joiner--homepage (lm-homepage fancy-joiner--dot-el))

;;;; Customizable variables

(defgroup fancy-joiner nil
  (format "%s." fancy-joiner--summary)
  :group 'convenience
  :link  '(emacs-library-link :tag "Lisp file" "fancy-joiner.el")
  :link  `(file-link :tag "README.org" ,fancy-joiner--readme-org)
  :link  `(url-link  :tag "Homepage"   ,fancy-joiner--homepage))

(defcustom fancy-joiner-leave-pipes-at-eol t
  "In ‘sh-mode’, whether to leave pipes at end of line after splitting.

Suppose you have this piece of code:
        echo things | tr -d s | sed 's/$/y/'

If non-nil, after fancy-joiner-splitting your code looks like this:
        echo things |
            tr -d s |
            sed 's/$/y/'

If nil, after fancy-joiner-splitting your code looks like this:
        echo things \\
            | tr -d s \\
            | sed 's/$/y/'

Although the latter seems to be more popular, the author prefers
the former, and believes that the latter’s popularity is mostly due
to people not knowing that pipes can be left alone just fine at the
end of the line."
  :type  'boolean
  :group 'fancy-joiner)

(defcustom fancy-joiner-aggressive-indent-when-split-backward-in-sh-mode nil
  "If ‘aggressive-indent-mode’ is installed, when non-nil, force indent.

So try to indent at every press of ‘fancy-joiner-split-backward’.
This means greater chance to see it indented when you hold the key
down to split many lines, but it may come at the cost of a delayed
response.

Note that ‘aggressive-indent-mode’ would respond fine after each
operation if they are discrete and have a bit of time between them,
so this option is probably only useful if you often hold down the
key to split larger chunks of texts and prefer to make sure it gets
indented, in spite of a greater lag."
  :type  'boolean
  :group 'fancy-joiner)

(defcustom fancy-joiner-aggressive-indent-when-split-forward-in-sh-mode nil
  "If ‘aggressive-indent-mode’ is installed, when non-nil, force indent.

So try to indent at every press of ‘fancy-joiner-split-forward’.
This means greater chance to see it indented when you hold the key
down to split many lines, but it may come at the cost of a delayed
response.

Note that ‘aggressive-indent-mode’ would respond fine after each
operation if they are discrete and have a bit of time between them,
so this option is probably only useful if you often hold down the
key to split larger chunks of texts and prefer to make sure it gets
indented, in spite of a greater lag."
  :type  'boolean
  :group 'fancy-joiner)

;;;; Commands
;;;;; Minor modes

;; Note: I had a problem with one or another shifted key being automatically
;; “translated” into its non-shifted version, thereby not triggering the right
;; command. I’m not sure of what exactly influences this, but there seems to
;; be some such translated issue when using keyboard notation (such as M-J, or
;; even M-S-j. Using vector notation solved it for me.

;;;;;; non-sh

(defvar fancy-joiner-non-sh-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map [(meta    shift ?j)] 'fancy-joiner-join-backward)
    (define-key map [(meta          ?j)] 'fancy-joiner-join-forward)
    (define-key map [(super   shift ?j)] 'fancy-joiner-absorb-backward)
    (define-key map [(super         ?j)] 'fancy-joiner-absorb-forward)
    map))

(defcustom fancy-joiner-non-sh-mode-lighter " fj"
  "Mode line lighter for ‘fancy-joiner-non-sh-mode’.
Either a string to display in the mode line when
fancy-joiner-non-sh-mode’ is on, or nil to display nothing."
  :group 'fancy-joiner
  :type '(choice (string :tag "Lighter" :value " fj")
                 (const  :tag "Nothing" nil)))

;;;###autoload
(define-minor-mode fancy-joiner-non-sh-mode
  "Enable keys for ‘fancy-joiner’ outside ‘sh-mode’.
Besides ‘sh-mode’, ‘dired-mode’ and ‘special-mode’ are also excluded.

See also ‘fancy-joiner-non-sh-mode-lighter’ and
global-fancy-joiner-non-sh-mode’."
  :init-value nil
  :lighter fancy-joiner-non-sh-mode-lighter
  :keymap  fancy-joiner-non-sh-mode-map
  :group  'fancy-joiner)

(defun fancy-joiner--turn-on-non-sh-mode ()
  "Enable ‘fancy-joiner-non-sh-mode’ globally."
  (or (derived-mode-p #'sh-mode)
      (derived-mode-p #'dired-mode)
      (derived-mode-p #'special-mode)
      (fancy-joiner-non-sh-mode)))

;;;###autoload
(define-globalized-minor-mode global-fancy-joiner-non-sh-mode
  fancy-joiner-non-sh-mode  fancy-joiner--turn-on-non-sh-mode
  :group 'fancy-joiner)

;;;;;; sh

(defvar fancy-joiner-sh-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map [(meta    shift ?j)] 'fancy-joiner-join-backward)
    (define-key map [(meta          ?j)] 'fancy-joiner-join-forward)
    (define-key map [(super   shift ?j)] 'fancy-joiner-absorb-backward)
    (define-key map [(super         ?j)] 'fancy-joiner-absorb-forward)
    (define-key map [(control shift ?j)] 'fancy-joiner-split-backward)
    (define-key map [(control       ?j)] 'fancy-joiner-split-forward)
    map))

(defcustom fancy-joiner-sh-mode-lighter " fj"
  "Mode line lighter for ‘fancy-joiner-sh-mode’.
Either a string to display in the mode line when
fancy-joiner-sh-mode’ is on, or nil to display nothing."
  :group 'fancy-joiner
  :type '(choice (string :tag "Lighter" :value " fj")
                 (const  :tag "Nothing" nil)))

;;;###autoload
(define-minor-mode fancy-joiner-sh-mode
  "Enable keys for ‘fancy-joiner’ outside ‘sh-mode’.
See also ‘fancy-joiner-sh-mode-lighter’ and
global-fancy-joiner-sh-mode’."
  :init-value nil
  :lighter fancy-joiner-sh-mode-lighter
  :keymap  fancy-joiner-sh-mode-map
  :group  'fancy-joiner)

(defun fancy-joiner--turn-on-sh-mode ()
  "Enable ‘fancy-joiner-sh-mode’ globally."
  (when (derived-mode-p #'sh-mode)
    (fancy-joiner-sh-mode)))

;;;###autoload
(define-globalized-minor-mode global-fancy-joiner-sh-mode
  fancy-joiner-sh-mode  fancy-joiner--turn-on-sh-mode
  :group 'fancy-joiner)

;;;;; Joining, absorbing, and splitting

;;;;;; Main

;;;###autoload
(defun fancy-joiner-join-forward ()
  "Join current line with next line, removing spaces.
When not in ‘shell-script-mode’, this is essentially the same
as (join-line t). When in ‘shell-script-mode’ but outside comments,
it automagically manages the insertion of [;|&] and other such
end-of-command markers; inside comments, it works like an improved
fancy-joiner-absorb-forward’."
  (interactive)
  (fancy-joiner-join 'forward))

;;;###autoload
(defun fancy-joiner-join-backward ()
  "Join current line with previous line, removing spaces.
When not in ‘shell-script-mode’, this is essentially the same
as (join-line nil). When in ‘shell-script-mode’ but outside
comments, it automagically manages the insertion of [;|&] and other
such end-of-command markers; inside comments, it works like an
improved ‘fancy-joiner-absorb-backward’."
  (interactive)
  (fancy-joiner-join))

;;;###autoload
(defun fancy-joiner-absorb-forward ()
  "Join current line with next line, removing various characters.
It removes dashes, asterisks, pluses, and other markers present at
the beginning of the next line if they were followed by at least a
space. Handy to quickly merge sequential org headings, items in org
lists, comment characters ([;#]), etc.

In ‘shell-script-mode’, it only makes sense to use it in commented
lines. But since ‘fancy-joiner-join-forward’ does the same job in
this case, you don’t really need it, so you can do all joinings
with the same key."
  (interactive)
  (fancy-joiner-join-forward)
  ;; No zapping in sh-mode, because the fancy-joiner-join above is already
  ;; detecting commented lines (see cm•cm), and zapping would end up killing
  ;; an extra word — so we correct that. There doesn’t seem to be any other
  ;; case for zapping/absorbing in sh-mode other than commented lines.
  (pcase major-mode
    ('sh-mode (ignore))
    (_        (fancy-joiner-zap-up-to-space))))

;;;###autoload
(defun fancy-joiner-absorb-backward ()
  "Join current line with previous line, removing various characters.
It removes dashes, asterisks, pluses, and other markers present at
the beginning of the next line if they were followed by at least a
space. Handy to quickly merge sequential org headings, items in org
lists, comment characters ([;#]), etc.

In ‘shell-script-mode’, it only makes sense to use it in commented
lines. But since ‘fancy-joiner-join-backward’ does the same job in
this case, you don’t really need it, so you can do all joinings
with the same key."
  (interactive)
  (fancy-joiner-join-backward)
  ;; No zapping in sh-mode, because the fancy-joiner-join above is already
  ;; detecting commented lines (see cm•cm), and zapping would end up killing
  ;; an extra word — so we correct that. There doesn’t seem to be any other
  ;; case for zapping/absorbing in sh-mode other than commented lines.
  (pcase major-mode
    ('sh-mode (ignore))
    (_        (fancy-joiner-zap-up-to-space))))

;;;###autoload
(defun fancy-joiner-split-forward ()
  "Split in two a line containing multiple commands in ‘sh-mode’.
It tries to be roughly the opposite of what the result of
fancy-joiner-join-forward’ is. To use it, point must be at the
left of a splitting combination of characters such as [&|;] or the
first part of a case-esac statement."
  (interactive)
  (fancy-joiner-split 'forward))

;;;###autoload
(defun fancy-joiner-split-backward ()
  "Split in two a line containing multiple commands in ‘sh-mode’.
It tries to be roughly the opposite of what the result of
fancy-joiner-join-backward’ is. To use it, point must be at the
right of a splitting combination of characters such as [&|;] or the
first part of a case-esac statement."
  (interactive)
  (fancy-joiner-split))

;;;;;; Meta

(defun fancy-joiner-join (&optional bool)
  "Join lines autodetecting context. In ‘sh-mode’, deal with semicolons.

If BOOL is non-nil, do it forward.
If BOOL is     nil, do it backward.

Note that there’s no attempt to fix indentation.
So it works best when using some nice indentation package such as
aggressive-indent-mode’."
  (interactive)
  (pcase major-mode
    ('sh-mode
     (join-line bool)
     (when (looking-back "[^;];" (point-at-bol))
       (delete-char -1))
     (let* ((bol•   (looking-back (rx  bol (0+ blank)) (point-at-bol)))
            (•eol   (looking-at   (rx (0+ blank) eol)))
            (cm•cm  (and (looking-at   " *#")
                         (looking-back "#.*" (point-at-bol))))
            (•}     (and (looking-at "}")
                         (not (looking-back
                               (rx (or bol (in ";&|")) (0+ blank))
                               (point-at-bol)))))
            ({•     (looking-back "[{]"    (point-at-bol)))
            (•cp    (looking-at   (rx (0+ blank) ")")))
            (op•    (looking-back "[(]"    (point-at-bol)))
            (opcp•  (looking-back "[(][)]" (point-at-bol)))
            (bs•    (looking-back "[\\]"   (point-at-bol)))
            (|&•    (looking-back "[|&]"   (point-at-bol)))
            (sc×2•  (looking-back ";;"     (point-at-bol)))
            (•sc×2  (looking-at " *;;"))
            (•|&    (looking-at " *[|&]"))
            (word  `("do" "elif" "else" "if" "in" "then"))
            (wd•    (looking-back (regexp-opt word 'words) (point-at-bol))))

       (cond (bs•    (delete-char 1) (delete-char -1))
             ((or  bol• •eol •cp op• |&• sc×2• •sc×2 •|& opcp• wd•) (ignore))
             ;; We act like a smarter fancy-joiner-absorb if a comment is
             ;; found, since we try to detect if we are already inside a
             ;; comment.
             (cm•cm  (fancy-joiner-zap-up-to-space))
             ;; (•cp    (delete-char 1)) ; why did I add that?
             (•}     (insert " ;"))
             ({•     (insert " "))
             (t      (insert ";")))))
    (_ (join-line bool))))

(defun fancy-joiner-split (&optional bool)
  "Split lines autodetecting context. In ‘sh-mode’, deal with semicolons.

If BOOL is non-nil, do it forward.
If BOOL is     nil, do it backward.

Note that there’s no attempt to fix indentation.
So it works best when using some nice indentation package such as
aggressive-indent-mode’."
  (interactive)
  (pcase major-mode
    ('sh-mode
     (let* ((tr  '(";;&"))
            (db  '(";;"  ";&"  "&&"  "&|"  "|&"   "||"))
            (sg  '(";"         "&"         "|"        ))
            ({sp (rx "{" blank))
            (cs  (rx bow "case" (1+ blank) (1+ (not (in ";&|")))
                     (1+ blank) "in" blank))
            (re  (concat (regexp-opt sg) "\\|" {sp "\\|" cs)))
       ;; Not used, but left here as reference.
       (ignore tr db)
       ;; Unifying backward and forward didn’t work so well. So I separate
       ;; them, in spite of some repetition.
       (pcase bool
         ;; Splitting backward
         ('nil
          (when (re-search-backward re (point-at-bol) t 1)
            (let ((sc×2•&     (looking-back ";;"    (point-at-bol)))
                  (sc&|•sc&|  (looking-back "[;&|]" (point-at-bol)))
                  (•|         (looking-at   "|"))
                  (•{&        (looking-at   "[{&]"))
                  (•sc        (looking-at   ";"))
                  (•case      (looking-at   "case")))
              (cond (sc×2•&     (fancy-joiner--fob 1 1 3))
                    (sc&|•sc&|  (if (not (looking-at "[&|][[:blank:]]*{"))
                                    (fancy-joiner--fob 1 1 2)
                                  (backward-char 1)
                                  (fancy-joiner-split)))
                    (•|         (if (looking-at (rx "|" (0+ blank) "{"))
                                    (fancy-joiner-split)
                                  (if fancy-joiner-leave-pipes-at-eol
                                      (fancy-joiner--fob 1 1 1)
                                    (open-line 1)
                                    (insert "\\"))))
                    (•{&        (fancy-joiner--fob 1 1 1))
                    (•sc        (delete-char 1)
                                (open-line 1))
                    (•case      (re-search-forward (rx blank "in" blank)
                                                   (point-at-eol) t 1)
                                (fancy-joiner--fob 0 1 3))))
            (delete-trailing-whitespace (point-at-bol) (point-at-eol))
            (and fancy-joiner-aggressive-indent-when-split-backward-in-sh-mode
                 (fboundp 'aggressive-indent-indent-defun)
                 (aggressive-indent-indent-defun))))

         ;; Splitting forward
         (_
          (when (re-search-forward re (point-at-eol) t 1)
            (let ((sc•sc&     (looking-at   ";&"))
                  (sc&|•sc&|  (looking-at   "[;&|]"))
                  (|•         (looking-back "|"    (point-at-bol)))
                  (sc•        (looking-back ";"    (point-at-bol)))
                  (amp•       (looking-back "&"    (point-at-bol)))
                  ({insp•     (looking-back (rx (or "{" "in") blank)
                                            (point-at-bol))))
              (cond (sc•sc&     (fancy-joiner--fob 2 1 0 'fl))
                    (sc&|•sc&|  (if (not (looking-at "[&|][[:blank:]]*{"))
                                    (fancy-joiner--fob 1 1 0 'fl)
                                  (forward-char 1)
                                  (fancy-joiner-split 'forward)))
                    (|•         (if (or (looking-at   (rx (0+ blank) "{"))
                                        (looking-back (rx bol (0+ blank) "|")
                                                      (point-at-bol)))
                                    (fancy-joiner-split 'forward)
                                  (unless fancy-joiner-leave-pipes-at-eol
                                    (backward-char 1)
                                    (insert "\\"))
                                  (fancy-joiner--fob 0 1 0 'fl)))
                    (sc•        (delete-char -1)
                                (fancy-joiner--fob 0 1 0 'fl))
                    (amp•       (fancy-joiner--fob 0 1 0 'fl))
                    ({insp•     (fancy-joiner--fob 0 1 0 'fl))))
            (and fancy-joiner-aggressive-indent-when-split-forward-in-sh-mode
                 (fboundp 'aggressive-indent-indent-defun)
                 (aggressive-indent-indent-defun)))))))
    ;; Other modes
    (_ (ignore))))

;;;;;; Other

(defun fancy-joiner-zap-up-to-space ()
  "Delete everything up to the next space, not included."
  (interactive)
  (kill-region (point) (progn
                         (forward-char 1)
                         (unwind-protect
                             (search-forward " " nil nil 1)
                           (backward-char 1))
                         (point))))

;;;;;; Helper

(defun fancy-joiner--fob (&optional f o b fl)
  "Forward F characters, open O lines, backward B characters.
When one or more of F, O or B are nil, they default to 1.
When FL is non-nil, delete trailing whitespace and then move point
to the beginning of the next line."
  (forward-char  (or f 1))
  (open-line     (or o 1))
  (backward-char (or b 1))
  (when fl
    (delete-trailing-whitespace (point-at-bol) (point-at-eol))
    (forward-line 1)))

;;;;; See README

;;;###autoload
(defun fancy-joiner-see-readme (&optional heading narrow)
  "Open fancy-joiner's README.org file.
Search for the file in fancy-joiner.el's directory.

If found, open it read-only.

If optional argument HEADING is passed, try to navigate to the
heading after opening it. HEADING should be a string.

If optional argument NARROW is non-nil, narrow to that heading.
This argument has no effect if HEADING is nil or not found.

Examples:
  (fancy-joiner-see-readme \"News\" t)
  (fancy-joiner-see-readme \"Installation\")"
  (interactive)
  (let ((readme fancy-joiner--readme-org))
    (if (file-exists-p readme)
        (let ((pr (make-progress-reporter
                   (format "Opening %s ... "
                           (abbreviate-file-name readme)))))
          (find-file-read-only readme)
          (when heading
            (fancy-joiner--goto-org-heading heading narrow))
          (progress-reporter-done pr))
      (message "Couldn't find %s's README.org" fancy-joiner--name))))

(defun fancy-joiner--goto-org-heading (heading &optional narrow)
  "Navigate to org HEADING and optionally NARROW to it."
  (let* ((hrx (format "^[*]+ %s" heading))
         (pos (save-match-data
                (save-excursion
                  (save-restriction
                    (widen)
                    (goto-char (point-max))
                    (re-search-backward hrx nil t 1))))))
    (when pos
      (widen)
      (goto-char pos)
      (if (and narrow (fboundp 'org-narrow-to-subtree))
          (org-narrow-to-subtree)
        (recenter-top-bottom 1))
      (when (fboundp 'outline-show-subtree)
        (outline-show-subtree)))))

;;;;; See News

;;;###autoload
(defun fancy-joiner-see-news ()
  "See the News in fancy-joiner's README.org file."
  (interactive)
  (fancy-joiner-see-readme "News" 'narrow)
  (let ((cmds '(outline-hide-subtree
                outline-show-children
                outline-next-heading
                outline-show-subtree)))
    (and (fboundp (nth 0 cmds))
         (fboundp (nth 1 cmds))
         (fboundp (nth 2 cmds))
         (fboundp (nth 3 cmds))
         (mapc #'funcall cmds))))

;;;; Wrapping up

(provide 'fancy-joiner)

;; Local Variables:
;; coding:                     utf-8
;; indent-tabs-mode:           nil
;; sentence-end-double-space:  nil
;; outline-regexp:             ";;;;* "
;; End:

;;; fancy-joiner.el ends here