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.
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
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
Keybindings that I use: C-S-j and C-j for backward and forward, respectively.
("C" is the Ctrl key)
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
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:
- 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". - 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.
- 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