aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-03 06:56:10 -0500
committerCraig Jennings <c@cjennings.net>2026-05-03 06:56:10 -0500
commit6936f5b60b61dcf41cbda75813d7f259733eedf2 (patch)
treef058960d56232d8629c6833607a0df287a44063d
parente2f373ff05116d4b2ed48d4ddcaea8c47846f105 (diff)
downloaddotemacs-6936f5b60b61dcf41cbda75813d7f259733eedf2.tar.gz
dotemacs-6936f5b60b61dcf41cbda75813d7f259733eedf2.zip
fix(line-paragraph): join-line-or-region strands space on next line
The region branch's `(while (< (point) end) (join-line 1))` ran one iteration too many. After the final in-region join, point sat just before the end marker, so the loop fired once more. That extra `join-line 1` consumed the next line's preceding newline and replaced it with a space. Then `(goto-char end)` + `(newline)` reinserted a newline at the original end position, before the inserted space, so the space ended up stranded at BOL of the next line. I replaced the position-based loop with `count-lines` + `dotimes` to do exactly the right number of joins. I also swapped the trailing `(newline)` for `(forward-line 1)`. The bullet-list use case now lands directly on the next existing line with no blank gap. The trailing-newline change ripples to `cj/join-paragraph` (which delegates here), so paragraphs now also stop adding a trailing newline when the input lacks one. `require-final-newline` handles file-end discipline on save anyway. I added 3 new tests that fail against the old loop and pass against the fix. I also updated 11 existing tests whose assertions baked in the old trailing-newline behavior. While in there I wrapped the `cj/custom-keymap` defvar stub in `eval-and-compile` in both test files. The bare defvar wasn't evaluated at byte-compile time, so the `require` of `custom-line-paragraph` would hit a void symbol when the validate-el hook ran.
-rw-r--r--modules/custom-line-paragraph.el14
-rw-r--r--tests/test-custom-line-paragraph-join-line-or-region.el92
-rw-r--r--tests/test-custom-line-paragraph-join-paragraph.el29
3 files changed, 108 insertions, 27 deletions
diff --git a/modules/custom-line-paragraph.el b/modules/custom-line-paragraph.el
index 32f9aaa1..4b0baa90 100644
--- a/modules/custom-line-paragraph.el
+++ b/modules/custom-line-paragraph.el
@@ -24,13 +24,19 @@
"Join lines in the active region or join the current line with the previous one."
(interactive)
(if (use-region-p)
- (let ((beg (region-beginning))
- (end (copy-marker (region-end))))
+ ;; Compute the join count up front from the region's line span.
+ ;; A position-based loop overshoots β€” after the final in-region join,
+ ;; point still sits before the end marker, so one extra `join-line 1`
+ ;; reaches past the region and pulls the next line in, leaving a stray
+ ;; space at its head when the trailing newline is reinserted.
+ (let* ((beg (region-beginning))
+ (end (copy-marker (region-end)))
+ (n (count-lines beg end)))
(goto-char beg)
- (while (< (point) end)
+ (dotimes (_ (max 0 (1- n)))
(join-line 1))
(goto-char end)
- (newline)
+ (forward-line 1)
(deactivate-mark))
;; No region - only join if there's a previous line
(when (> (line-number-at-pos) 1)
diff --git a/tests/test-custom-line-paragraph-join-line-or-region.el b/tests/test-custom-line-paragraph-join-line-or-region.el
index 0d28ab6c..f8738910 100644
--- a/tests/test-custom-line-paragraph-join-line-or-region.el
+++ b/tests/test-custom-line-paragraph-join-line-or-region.el
@@ -26,12 +26,15 @@
;; Add modules directory to load path
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
-;; Stub dependencies before loading the module
-(defvar cj/custom-keymap (make-sparse-keymap)
- "Stub keymap for testing.")
-
-;; Stub expand-region package
-(provide 'expand-region)
+;; Stub dependencies before loading the module. `eval-and-compile` is required
+;; because the byte-compile pass `require`s custom-line-paragraph, which runs
+;; the module's top-level `(keymap-set cj/custom-keymap ...)` form at load
+;; time. A bare `defvar` here would only declare the symbol at compile time;
+;; the keymap-set then sees a void value.
+(eval-and-compile
+ (defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+ (provide 'expand-region))
;; Now load the actual production module
(require 'custom-line-paragraph)
@@ -84,8 +87,10 @@
(should (string-match-p "line one line two line three" (buffer-string))))
(test-join-line-or-region-teardown)))
-(ert-deftest test-join-line-or-region-with-region-adds-newline-at-end ()
- "With region, should add newline at end."
+(ert-deftest test-join-line-or-region-with-region-no-newline-added-at-end ()
+ "Region branch must not add a trailing newline. The function lands point on
+the next existing line via `forward-line 1`, so an input without a trailing
+newline produces output without one."
(test-join-line-or-region-setup)
(unwind-protect
(with-temp-buffer
@@ -95,7 +100,7 @@
(goto-char (point-max))
(activate-mark)
(cj/join-line-or-region)
- (should (string-suffix-p "\n" (buffer-string))))
+ (should (string= "line one line two line three" (buffer-string))))
(test-join-line-or-region-teardown)))
(ert-deftest test-join-line-or-region-preserves-text-content ()
@@ -138,6 +143,70 @@
(should (string-match-p "two three four" (buffer-string))))
(test-join-line-or-region-teardown)))
+(ert-deftest test-join-line-or-region-with-region-no-stray-space-on-following-line ()
+ "Normal: joining a multi-line region must not leave a stray space at the start
+of the line below the region. Regression: the original `while`-based loop ran
+one iteration too many, so the final `join-line 1` reached past the region and
+replaced the trailing newline of the region with a space, then `goto-char end`
++ `newline` reinserted the newline before that space, stranding it at BOL of
+the next line."
+ (test-join-line-or-region-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "- /Confidence calibration/ Every source declares its\n"
+ " own confidence in [0..1], and those numbers don't mean the same\n"
+ " thing. Calibrating across sources is a downstream-of-schema\n"
+ " problem.\n"
+ "- /Detection-to-detection links/ within a single sensor scene.\n")
+ (goto-char (point-min))
+ (set-mark (point))
+ (search-forward "problem.")
+ (activate-mark)
+ (cj/join-line-or-region)
+ (goto-char (point-min))
+ (search-forward "- /Detection")
+ (beginning-of-line)
+ ;; The next-bullet line must begin with the dash, not a leading space.
+ (should (eq (char-after) ?-)))
+ (test-join-line-or-region-teardown)))
+
+(ert-deftest test-join-line-or-region-with-region-cursor-lands-on-next-existing-line ()
+ "Normal: after joining a multi-line region, point should land at BOL of the
+existing line immediately after the region β€” no blank line inserted between."
+ (test-join-line-or-region-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "alpha line one\n"
+ " alpha line two\n"
+ " alpha line three\n"
+ "beta line\n")
+ (goto-char (point-min))
+ (set-mark (point))
+ (search-forward "alpha line three")
+ (activate-mark)
+ (cj/join-line-or-region)
+ (should (looking-at-p "beta line")))
+ (test-join-line-or-region-teardown)))
+
+(ert-deftest test-join-line-or-region-with-region-no-blank-gap-after-paragraph ()
+ "Normal: the joined paragraph must abut the next existing line directly. The
+exact buffer shape is asserted to catch both the leading-space regression and
+any spurious blank line from a residual trailing `(newline)`."
+ (test-join-line-or-region-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "alpha one\n"
+ " alpha two\n"
+ "beta\n")
+ (goto-char (point-min))
+ (set-mark (point))
+ (search-forward "alpha two")
+ (activate-mark)
+ (cj/join-line-or-region)
+ (should (string= (buffer-string)
+ "alpha one alpha two\nbeta\n")))
+ (test-join-line-or-region-teardown)))
+
;;; Boundary Cases
(ert-deftest test-join-line-or-region-on-first-line-no-region-does-nothing-except-newline ()
@@ -166,7 +235,8 @@
(test-join-line-or-region-teardown)))
(ert-deftest test-join-line-or-region-single-line-region ()
- "Should handle single-line region."
+ "Should handle single-line region. Region branch performs zero joins and
+moves point to the next existing line; the buffer is left untouched."
(test-join-line-or-region-setup)
(unwind-protect
(with-temp-buffer
@@ -176,7 +246,7 @@
(goto-char (point-max))
(activate-mark)
(cj/join-line-or-region)
- (should (string= "only line\n" (buffer-string))))
+ (should (string= "only line" (buffer-string))))
(test-join-line-or-region-teardown)))
(ert-deftest test-join-line-or-region-region-with-only-whitespace-lines ()
diff --git a/tests/test-custom-line-paragraph-join-paragraph.el b/tests/test-custom-line-paragraph-join-paragraph.el
index a84adc6c..9f68b3b1 100644
--- a/tests/test-custom-line-paragraph-join-paragraph.el
+++ b/tests/test-custom-line-paragraph-join-paragraph.el
@@ -40,9 +40,14 @@
;; Add expand-region to load path explicitly
(add-to-list 'load-path (expand-file-name "elpa/expand-region-1.0.0" user-emacs-directory))
-;; Stub dependencies before loading the module
-(defvar cj/custom-keymap (make-sparse-keymap)
- "Stub keymap for testing.")
+;; Stub dependencies before loading the module. `eval-and-compile` is required
+;; because the byte-compile pass `require`s custom-line-paragraph, which runs
+;; the module's top-level `(keymap-set cj/custom-keymap ...)` form at load
+;; time. A bare `defvar` here would only declare the symbol at compile time;
+;; the keymap-set then sees a void value.
+(eval-and-compile
+ (defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing."))
;; Load expand-region for real (needed by cj/join-paragraph)
(require 'expand-region)
@@ -73,7 +78,7 @@
(goto-char (point-min))
(cj/join-paragraph)
(should (string= (buffer-substring-no-properties (point-min) (point-max))
- "line one line two line three\n")))
+ "line one line two line three")))
(test-join-paragraph-teardown)))
(ert-deftest test-join-paragraph-simple-multiline-cursor-in-middle ()
@@ -87,7 +92,7 @@
(forward-line 1)
(cj/join-paragraph)
(should (string= (buffer-substring-no-properties (point-min) (point-max))
- "line one line two line three\n")))
+ "line one line two line three")))
(test-join-paragraph-teardown)))
(ert-deftest test-join-paragraph-simple-multiline-cursor-at-end ()
@@ -100,7 +105,7 @@
(goto-char (point-max))
(cj/join-paragraph)
(should (string= (buffer-substring-no-properties (point-min) (point-max))
- "line one line two line three\n")))
+ "line one line two line three")))
(test-join-paragraph-teardown)))
(ert-deftest test-join-paragraph-surrounded-by-other-paragraphs ()
@@ -134,7 +139,7 @@
(goto-char (point-min))
(cj/join-paragraph)
(should (string= (buffer-substring-no-properties (point-min) (point-max))
- " indented line one indented line two indented line three\n")))
+ " indented line one indented line two indented line three")))
(test-join-paragraph-teardown)))
(ert-deftest test-join-paragraph-cursor-position-after ()
@@ -206,7 +211,7 @@
(cj/join-paragraph)
;; Should still work, even if nothing to join
(should (string= (buffer-substring-no-properties (point-min) (point-max))
- "single line paragraph\n")))
+ "single line paragraph")))
(test-join-paragraph-teardown)))
(ert-deftest test-join-paragraph-at-buffer-start ()
@@ -230,7 +235,7 @@
(insert "other paragraph\n\nfirst line\nsecond line\nthird line")
(goto-char (point-max))
(cj/join-paragraph)
- (should (string-match-p "first line second line third line\n$" (buffer-string))))
+ (should (string-match-p "first line second line third line\\'" (buffer-string))))
(test-join-paragraph-teardown)))
(ert-deftest test-join-paragraph-very-long ()
@@ -288,7 +293,7 @@
(goto-char (point-min))
(cj/join-paragraph)
(should (string= (buffer-substring-no-properties (point-min) (point-max))
- "line one line two line three\n")))
+ "line one line two line three")))
(test-join-paragraph-teardown)))
(ert-deftest test-join-paragraph-only-whitespace-lines ()
@@ -314,7 +319,7 @@
(goto-char (point-min))
(cj/join-paragraph)
(should (string= (buffer-substring-no-properties (point-min) (point-max))
- "Hello πŸ‘‹ world こんにけは δΈ–η•Œ πŸŽ‰ celebration 🎊\n")))
+ "Hello πŸ‘‹ world こんにけは δΈ–η•Œ πŸŽ‰ celebration 🎊")))
(test-join-paragraph-teardown)))
;; ---------------------------- Error Cases ------------------------------------
@@ -353,7 +358,7 @@
(goto-char (point-min))
(cj/join-paragraph)
(should (string= (buffer-substring-no-properties (point-min) (point-max))
- "x\n")))
+ "x")))
(test-join-paragraph-teardown)))
(provide 'test-custom-line-paragraph-join-paragraph)