|
#!/usr/bin/env bash |
|
set -euo pipefail |
|
|
|
readonly SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) |
|
readonly JED_DIR=${HOME}/.jed |
|
readonly TARGET_ISEARCH=${JED_DIR}/isearch-emacs-word.sl |
|
readonly TARGET_JEDRC=${HOME}/.jedrc |
|
readonly ALIAS_FILE=${HOME}/.bash_aliases |
|
readonly MARKER='Emacs-like repeated C-w support' |
|
|
|
fail() { |
|
printf 'ERROR: %s\n' "$*" >&2 |
|
exit 1 |
|
} |
|
|
|
command -v jed >/dev/null 2>&1 || fail 'jed is not installed. Run: sudo apt update && sudo apt install -y jed' |
|
command -v python3 >/dev/null 2>&1 || fail 'python3 is required but was not found.' |
|
command -v dpkg >/dev/null 2>&1 || fail 'This installer expects a Debian/Ubuntu/Raspberry Pi OS system with dpkg.' |
|
|
|
[[ -f ${SCRIPT_DIR}/.jedrc ]] || fail "Missing ${SCRIPT_DIR}/.jedrc" |
|
|
|
upstream_isearch=$(dpkg -L jed-common 2>/dev/null | awk '/\/isearch\.sl$/ { print; exit }') |
|
[[ -n ${upstream_isearch} ]] || fail 'Could not locate isearch.sl from the jed-common package.' |
|
[[ -r ${upstream_isearch} ]] || fail "Cannot read ${upstream_isearch}" |
|
|
|
mkdir -p -- "$JED_DIR" |
|
|
|
# Always rebuild from the currently installed JED source. This makes package |
|
# upgrades safe and keeps this patch tied to the local distro version. |
|
cp -- "$upstream_isearch" "$TARGET_ISEARCH" |
|
|
|
python3 - "$TARGET_ISEARCH" <<'PY' |
|
from __future__ import annotations |
|
|
|
import sys |
|
from pathlib import Path |
|
|
|
path = Path(sys.argv[1]) |
|
text = path.read_text(encoding="utf-8") |
|
|
|
marker = "Emacs-like repeated C-w support" |
|
if marker in text: |
|
raise SystemExit(0) |
|
|
|
helper_anchor = "\ndefine isearch_dir (dir)\n" |
|
case_anchor = "\n#ifdef IBMPC_SYSTEM\n" |
|
|
|
if helper_anchor not in text: |
|
raise SystemExit("Unable to patch JED: isearch_dir anchor was not found") |
|
if case_anchor not in text: |
|
raise SystemExit("Unable to patch JED: control-key switch anchor was not found") |
|
|
|
helper = r''' |
|
|
|
% Emacs-like repeated C-w support |
|
% |
|
% During incremental search, C-w appends either: |
|
% * the remaining word characters at the end of the current match, or |
|
% * one separator character, such as a space or punctuation. |
|
% |
|
% Therefore C-s C-w C-w C-w can collect "word", then its separator, |
|
% then the following word, closely matching GNU Emacs behavior. |
|
private define isearch_yank_word_or_char (str) |
|
{ |
|
variable text = ""; |
|
variable start_point; |
|
|
|
if (Last_Search_Failed) |
|
return text; |
|
|
|
push_spot (); |
|
|
|
% JED leaves point at the beginning of the current match. |
|
% Move to the end of the text already present in the search string. |
|
if (strlen (str) and looking_at (str)) |
|
() = right (strlen (str)); |
|
|
|
% JED's plain fsearch/bsearch cannot match across lines. Do not yank a |
|
% newline, because doing so would create a search string that cannot match. |
|
if (eolp () or eobp ()) |
|
{ |
|
pop_spot (); |
|
return text; |
|
} |
|
|
|
push_mark (); |
|
start_point = _get_point (); |
|
|
|
% If point is on a word character, collect through the end of that word. |
|
skip_word_chars (); |
|
|
|
% Otherwise collect exactly one separator character. Repeated C-w presses |
|
% can therefore include spaces and punctuation before collecting a word. |
|
if (_get_point () == start_point) |
|
() = right (1); |
|
|
|
text = bufsubstr (); |
|
pop_mark (0); |
|
pop_spot (); |
|
return text; |
|
} |
|
''' |
|
|
|
case = r''' |
|
{ case 23 : % ^W |
|
variable yanked = isearch_yank_word_or_char (str); |
|
variable n; |
|
|
|
ifnot (strlen (yanked)) |
|
{ |
|
beep (); |
|
continue; |
|
} |
|
|
|
% Keep deletion/backtracking granular. One DEL removes one |
|
% character, even when a C-w press appended a complete word. |
|
n = strlen (yanked); |
|
while (n > 0) |
|
{ |
|
push_position (1); |
|
n--; |
|
} |
|
|
|
str += yanked; |
|
Last_Search_Failed = 0; |
|
|
|
% Treat this as printable input for the reverse-search shortcut |
|
% test below. |
|
c = ' '; |
|
} |
|
''' |
|
|
|
text = text.replace(helper_anchor, helper + helper_anchor, 1) |
|
text = text.replace(case_anchor, case + case_anchor, 1) |
|
path.write_text(text, encoding="utf-8") |
|
PY |
|
|
|
# Validate that JED can parse the patched search implementation before touching |
|
# the user's active startup file. |
|
if ! jed -batch -n -l "$TARGET_ISEARCH" >/dev/null 2>&1; then |
|
rm -f -- "$TARGET_ISEARCH" |
|
fail 'JED could not parse the patched isearch file. The active ~/.jedrc was not changed.' |
|
fi |
|
|
|
if [[ -e $TARGET_JEDRC ]]; then |
|
backup=${TARGET_JEDRC}.bak.$(date +%Y%m%d-%H%M%S) |
|
cp -a -- "$TARGET_JEDRC" "$backup" |
|
printf 'Backed up existing config to %s\n' "$backup" |
|
fi |
|
|
|
install -m 0644 -- "$SCRIPT_DIR/.jedrc" "$TARGET_JEDRC" |
|
|
|
mkdir -p -- "$(dirname -- "$ALIAS_FILE")" |
|
touch -- "$ALIAS_FILE" |
|
if ! grep -Fqx "alias emacs='jed'" "$ALIAS_FILE"; then |
|
printf "\nalias emacs='jed'\n" >> "$ALIAS_FILE" |
|
fi |
|
|
|
printf '\nInstalled:\n' |
|
printf ' %s\n' "$TARGET_JEDRC" |
|
printf ' %s\n' "$TARGET_ISEARCH" |
|
printf ' %s\n' "$ALIAS_FILE" |
|
printf '\nRun this in the current shell:\n' |
|
printf ' source %q\n' "$ALIAS_FILE" |
|
printf '\nThen start JED with either: jed FILE or emacs FILE\n' |