Re: [PATCH] feature: relative line numbers

Benno Schulenberg <[email protected]> Mon, 30 Jun 2025 15:27:35 +0200
Newsgroups gmane.editors.nano.devel
Message-ID <[email protected]>
Op 30-06-2025 om 00:26 schreef bsandro:
> nano is my go-to code editor for several years now, but I still miss the
> relative line numbers feature from vim that I was using before that.

Interesting.  I've tried it, but... don't like it.  What is the point
of relative line numbers when nano does not have the relative movement
commands that vim has?  Also, I find showing the absolute line number
for the current line (instead of zero) confusing.

So, I'm not going to apply the patch.  (An improved version is attached,
that updates the screen also when 'relativenumbers' is toggled.  Also,
I've renamed the option to 'relativenumbers', to stay in nano's style:
no abbreviations in the option names.)

But what I might apply is the other attached patch: allow nano to jump
a relative amount of lines.  Unfortunately, negative numbers already
mean "from end of buffer", so the patch accepts double signs to mean
"relative": ++100 will jump a hundred lines forward, --200 two hundred
lines back.  Anyone interested in having this?


Benno
0001-out-of-tree-feature-relative-line-numbers.patch (text/x-patch, 9.3 KB)
From 7996caeb1da22cb29c10b5ae6871578f2d660bd1 Mon Sep 17 00:00:00 2001
From: Brian Sandro <[email protected]>
Date: Mon, 30 Jun 2025 01:10:25 +0300
Subject: [PATCH] out-of-tree feature: relative line numbers

When line numbers are active, option 'set relativenumbers' in your
nanorc file will cause `nano` to show relative instead of absolute
line numbers.  There is also the bindable function 'relativenumbers'
(not bound by default) for toggling relative line numbers on and off.

This is modeled on the corresponding vim/neovim feature.

Signed-off-by: Brian Sandro <[email protected]>
Signed-off-by: Benno Schulenberg <[email protected]>
---
 doc/nano.texi        |  6 ++++++
 doc/nanorc.5         |  6 ++++++
 doc/sample.nanorc.in |  3 +++
 src/definitions.h    |  1 +
 src/global.c         |  2 ++
 src/nano.c           |  8 ++++++++
 src/rcfile.c         |  3 +++
 src/winio.c          | 12 +++++++++++-
 syntax/nanorc.nanorc |  4 ++--
 9 files changed, 42 insertions(+), 3 deletions(-)

diff --git a/doc/nano.texi b/doc/nano.texi
index 9bfe049e..f3f7b64f 100644
--- a/doc/nano.texi
+++ b/doc/nano.texi
@@ -1155,6 +1155,9 @@ either @kbd{Backspace} acts like Delete or @kbd{Delete} acts like Backspace.
 Do regular-expression searches by default.
 Regular expressions in @command{nano} are of the extended type (ERE).
 
+@item set relativenumbers
+Display relative line numbers (when line numbers are enabled).
+
 @item set saveonexit
 Save a changed buffer automatically on exit (@kbd{^X}); don't prompt.
 
@@ -1844,6 +1847,9 @@ Toggles the displaying of overlong lines on multiple screen lines.
 @item linenumbers
 Toggles the display of line numbers in front of the text.
 
+@item relativenumbers
+Toggles relative line numbers.
+
 @item whitespacedisplay
 Toggles the showing of whitespace.
 
diff --git a/doc/nanorc.5 b/doc/nanorc.5
index d73de26f..ae3db6f1 100644
--- a/doc/nanorc.5
+++ b/doc/nanorc.5
@@ -303,6 +303,9 @@ system either Backspace acts like Delete or Delete acts like Backspace.
 Do regular-expression searches by default.
 Regular expressions in \fBnano\fR are of the extended type (ERE).
 .TP
+.B set relativenumbers
+Display relative line numbers when line numbers are enabled.
+.TP
 .B set saveonexit
 Save a changed buffer automatically on exit (\fB^X\fR); don't prompt.
 .TP
@@ -987,6 +990,9 @@ Toggles the displaying of overlong lines on multiple screen lines.
 .B linenumbers
 Toggles the display of line numbers in front of the text.
 .TP
+.B relativenumbers
+Toggles relative line numbers.
+.TP
 .B whitespacedisplay
 Toggles the showing of whitespace.
 .TP
diff --git a/doc/sample.nanorc.in b/doc/sample.nanorc.in
index e559356f..0395af58 100644
--- a/doc/sample.nanorc.in
+++ b/doc/sample.nanorc.in
@@ -154,6 +154,9 @@
 ## Regular expressions are of the extended type (ERE).
 # set regexp
 
+## Display relative line numbers (when line numbers are enabled).
+# set relativenumbers
+
 ## Save a changed buffer automatically on exit; don't prompt.
 # set saveonexit
 
diff --git a/src/definitions.h b/src/definitions.h
index c73b4a82..5ab849c5 100644
--- a/src/definitions.h
+++ b/src/definitions.h
@@ -363,6 +363,7 @@ enum {
 	TRIM_BLANKS,
 	SHOW_CURSOR,
 	LINE_NUMBERS,
+	RELATIVE_NUMBERS,
 	AT_BLANKS,
 	AFTER_ENDS,
 	LET_THEM_ZAP,
diff --git a/src/global.c b/src/global.c
index a97ddf6a..3ac46424 100644
--- a/src/global.c
+++ b/src/global.c
@@ -1613,6 +1613,8 @@ const char *epithet_of_flag(int flag)
 			return N_("Soft wrapping of overlong lines");
 		case LINE_NUMBERS:
 			return N_("Line numbering");
+		case RELATIVE_NUMBERS:
+			return N_("Relative line numbers");
 		case WHITESPACE_DISPLAY:
 			return N_("Whitespace display");
 		case NO_SYNTAX:
diff --git a/src/nano.c b/src/nano.c
index b6146b72..f53737cf 100644
--- a/src/nano.c
+++ b/src/nano.c
@@ -1134,6 +1134,11 @@ void toggle_this(int flag)
 			}
 			break;
 #endif
+#ifdef ENABLE_LINENUMBERS
+		case RELATIVE_NUMBERS:
+			refresh_needed = TRUE;
+			break;
+#endif
 #ifdef ENABLE_MOUSE
 		case USE_MOUSE:
 			mouse_init();
@@ -1152,6 +1157,9 @@ void toggle_this(int flag)
 		if (ISSET(MINIBAR) || ISSET(ZERO) || LINES == 1)
 			return;
 
+	if (ISSET(LINE_NUMBERS) && flag == RELATIVE_NUMBERS)
+		return;
+
 	if (flag == NO_HELP || flag == NO_SYNTAX)
 		enabled = !enabled;
 
diff --git a/src/rcfile.c b/src/rcfile.c
index 6143f5fc..d24deb23 100644
--- a/src/rcfile.c
+++ b/src/rcfile.c
@@ -56,6 +56,7 @@ static const rcoption rcopts[] = {
 #endif
 #ifdef ENABLE_LINENUMBERS
 	{"linenumbers", LINE_NUMBERS},
+	{"relativenumbers", RELATIVE_NUMBERS},
 #endif
 #ifdef HAVE_LIBMAGIC
 	{"magic", USE_MAGIC},
@@ -457,6 +458,8 @@ keystruct *strtosc(const char *input)
 #ifdef ENABLE_LINENUMBERS
 		else if (!strcmp(input, "linenumbers"))
 			s->toggle = LINE_NUMBERS;
+		else if (!strcmp(input, "relativenumbers"))
+			s->toggle = RELATIVE_NUMBERS;
 #endif
 		else if (!strcmp(input, "whitespacedisplay"))
 			s->toggle = WHITESPACE_DISPLAY;
diff --git a/src/winio.c b/src/winio.c
index baaa96f4..dd8c0efd 100644
--- a/src/winio.c
+++ b/src/winio.c
@@ -2547,13 +2547,17 @@ void draw_row(int row, const char *converted, linestruct *line, size_t from_col)
 	/* If line numbering is switched on, put a line number in front of
 	 * the text -- but only for the parts that are not softwrapped. */
 	if (margin > 0) {
+		long int linenumber = line->lineno;
+
 		wattron(midwin, interface_color_pair[LINE_NUMBER]);
+		if (ISSET(RELATIVE_NUMBERS))
+			linenumber = labs(openfile->current->lineno - linenumber);
 #ifndef NANO_TINY
 		if (ISSET(SOFTWRAP) && from_col != 0)
 			mvwprintw(midwin, row, 0, "%*s", margin - 1, " ");
 		else
 #endif
-			mvwprintw(midwin, row, 0, "%*zd", margin - 1, line->lineno);
+			mvwprintw(midwin, row, 0, "%*zd", margin - 1, linenumber);
 		wattroff(midwin, interface_color_pair[LINE_NUMBER]);
 #ifndef NANO_TINY
 		if (line->has_anchor && (from_col == 0 || !ISSET(SOFTWRAP)))
@@ -3336,6 +3340,12 @@ void edit_redraw(linestruct *old_current, update_type manner)
 
 	openfile->placewewant = xplustabs();
 
+#ifdef ENABLE_LINENUMBERS
+	/* When doing relative line numbers, a full refresh is needed. */
+	if (ISSET(LINE_NUMBERS) && ISSET(RELATIVE_NUMBERS) && openfile->current != old_current)
+		refresh_needed = TRUE;
+#endif
+
 	/* If the current line is offscreen, scroll until it's onscreen. */
 	if (current_is_offscreen()) {
 		adjust_viewport(ISSET(JUMPY_SCROLLING) ? CENTERING : manner);
diff --git a/syntax/nanorc.nanorc b/syntax/nanorc.nanorc
index 18c55292..7c272a6a 100644
--- a/syntax/nanorc.nanorc
+++ b/syntax/nanorc.nanorc
@@ -14,7 +14,7 @@ color bold,purple "^[[:blank:]]*include[[:blank:]]+[^[:blank:]"]+"
 color lime "^[[:blank:]]*extendsyntax[[:blank:]]+[[:alpha:]]+[[:blank:]]+(i?color|header|magic|comment|formatter|linter|tabgives)[[:blank:]]+.*"
 
 # The arguments of commands
-color brightgreen "^[[:blank:]]*(set|unset)[[:blank:]]+(afterends|allow_insecure_backup|atblanks|autoindent|backup|boldtext|bookstyle|breaklonglines|casesensitive|colonparsing|constantshow|cutfromcursor|emptyline|historylog|indicator|jumpyscrolling|linenumbers|locking|magic|minibar|mouse|multibuffer|noconvert|nohelp|nonewlines|positionlog|preserve|quickblank|rawsequences|rebinddelete|regexp|saveonexit|showcursor|smarthome|softwrap|stateflags|tabstospaces|trimblanks|unix|whitespacedisplay|wordbounds|zap|zero)\>"
+color brightgreen "^[[:blank:]]*(set|unset)[[:blank:]]+(afterends|allow_insecure_backup|atblanks|autoindent|backup|boldtext|bookstyle|breaklonglines|casesensitive|colonparsing|constantshow|cutfromcursor|emptyline|historylog|indicator|jumpyscrolling|linenumbers|relativenumbers|locking|magic|minibar|mouse|multibuffer|noconvert|nohelp|nonewlines|positionlog|preserve|quickblank|rawsequences|rebinddelete|regexp|saveonexit|showcursor|smarthome|softwrap|stateflags|tabstospaces|trimblanks|unix|whitespacedisplay|wordbounds|zap|zero)\>"
 color brightgreen "^[[:blank:]]*set[[:blank:]]+(backupdir|brackets|errorcolor|functioncolor|keycolor|matchbrackets|minicolor|numbercolor|operatingdir|promptcolor|punct|quotestr|scrollercolor|selectedcolor|speller|spotlightcolor|statuscolor|stripecolor|titlecolor|whitespace|wordchars)[[:blank:]]+"
 color brightgreen "^[[:blank:]]*set[[:blank:]]+(fill[[:blank:]]+-?[[:digit:]]+|(guidestripe|tabsize)[[:blank:]]+[1-9][0-9]*)\>"
 color brightgreen "^[[:blank:]]*bind[[:blank:]]+((\^([[:alpha:]]|[]/@\^_`-]|Space)|([Ss][Hh]-)?[Mm]-[[:alpha:]]|[Mm]-([][!"#$%&'()*+,./0-9:;<=>?@\^_`{|}~-]|Space))|F([1-9]|1[0-9]|2[0-4])|Ins|Del)[[:blank:]]+([[:lower:]]+|".*")[[:blank:]]+(main|help|search|replace(with)?|yesno|gotoline|writeout|insert|execute|browser|whereisfile|gotodir|spell|linter|all)\>"
@@ -32,7 +32,7 @@ color crimson "\{(location|gotoline|(begin|end)para|comment|complete|(un)?indent
 color crimson "\{(left|right|up|down|home|end|(scroll|page)(up|down)|(top|bottom)row|center|cycle|(prev|next)(word|block|anchor|buf))\}"
 color crimson "\{(tab|enter|delete|backspace|verbatim|refresh|suspend|casesens|regexp|backwards|older|newer|(dos|mac)format)\}"
 color crimson "\{(append|prepend|backup|flip(goto|replace|execute|pipe|convert|newbuffer)|browser|gotodir|(first|last)(file|line))\}"
-color crimson "\{(nohelp|constantshow|softwrap|linenumbers|whitespacedisplay|nosyntax|zero)\}"
+color crimson "\{(nohelp|constantshow|softwrap|linenumbers|relativenumbers|whitespacedisplay|nosyntax|zero)\}"
 color crimson "\{(smarthome|autoindent|cutfromcursor|breaklonglines|tabstospaces|mouse|\{)\}"
 
 # Commands
-- 
2.48.1
jump-relative-double-plus-or-double-minus.patch (text/x-patch, 739 B)
diff --git a/src/search.c b/src/search.c
index e2e6f685..af41cdd4 100644
--- a/src/search.c
+++ b/src/search.c
@@ -792,11 +792,19 @@ void goto_line_and_column(ssize_t line, ssize_t column, bool retain_answer,
 		if (response > 0)
 			return;
 
+		bool relative = (*answer == '-' && answer[1] == '-') || (*answer == '+' && answer[1] == '+');
+
+		if (relative)
+			memmove(answer, answer + 1, strlen(answer));
+
 		/* Try to extract one or two numbers from the user's response. */
 		if (!parse_line_column(answer, &line, &column)) {
 			statusline(AHEM, _("Invalid line or column number"));
 			return;
 		}
+
+		if (relative)
+			line = openfile->current->lineno + line;
 	} else {
 		if (line == 0)
 			line = openfile->current->lineno;
OpenPGP_signature.asc (application/pgp-signature, 840 B)
-----BEGIN PGP SIGNATURE-----

wsF5BAABCAAjFiEEFo5vQpe/16ea/USWUUu+Lrjhlh8FAmhikMcFAwAAAAAACgkQUUu+Lrjhlh8L
0A/+IyU73QxWZROk2Ui1DOhFcY3TPN+WLZ70u76/MrpR1dKNJfCMt2mvT+THOf3YZlIuBom0j2xt
FAlt+FKcuE3LR4gKC2ar/QcPSEP7lIjHisCYV5R8e94CbqXgo6C2BFhS16avkcQN0SwyG+HylgbF
yaE0LrPrRSMkyG0fM4oFaMdBO/6hhAEriZZ+qZWE1WrCIu8B/lsSklbQopZ5dHmYU8axhyYcEssy
XPXFx0qZQTA1s+HjVvTTOocooi+dQWjqF5t8tOT+MwpHasaVV8xqrRo4P1DHRc416oRCTMBZ/4fe
wXX+B5M+t/yD/jJKwCnjlcZv95ZWQp5hfTQTEGsHRQeTQfIodFcOxpqhIXcRg0bZUwysg2oOfhN2
KHj+7IKQULHrAwuz/Va2i59556+j7A94ALpMYZMz2YIO0F//3BaoCQLyUhwqyBMj/eLLngmsRrGJ
w5pdCzTyGsE3abNifQB4VEkqipJ0ONGGPH7dYILPbs/HcOJxBAcGoYFAZW81HG81ML9P5w3FRo37
+nPJQzrfG2ViB6nDDWEWuMLwmzLxRjZBrD96AWIKJSzRCXBU6mZLEKisBIOIjyV8ikb1wY9V6XfO
/w+gIElqbWhpL6rCePY6AIuh8vY6EZSdkbC1DHSCQFi9lH7lWTRhJiHqNulgG3S+ZUKUUCQf/nkV
Ybk=
=7rYw
-----END PGP SIGNATURE-----