From cb9c091336a5dea698fa3bc3937d12c54afea7d4 Mon Sep 17 00:00:00 2001
From: Benno Schulenberg <bensberg@telfort.nl>
Date: Fri, 27 Mar 2020 11:36:07 +0100
Subject: [PATCH 2/2] new feature: "knots" -- a kind of bookmarks, that can be
 cycled through

With M-' a knot is placed at the current line -- or removed when there
already is one.  With M-PageUp / M-PageDown one can jump to the nearest
knot in the upward and downward direction.  The search for a knot wraps
at the top and bottom.  A knot is not visible in any way.

This fulfills https://savannah.gnu.org/bugs/?57577.
Requested-by: Ken Tyler <kent@werple.net.au>
---
 src/cut.c    | 14 +++++++-
 src/files.c  |  5 +++
 src/global.c | 13 ++++++++
 src/nano.h   |  8 +++++
 src/proto.h  |  4 +++
 src/text.c   | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 134 insertions(+), 1 deletion(-)

diff --git a/src/cut.c b/src/cut.c
index 4dda9616..193d1964 100644
--- a/src/cut.c
+++ b/src/cut.c
@@ -87,6 +87,9 @@ void do_deletion(undo_type action)
 			openfile->mark = openfile->current;
 			openfile->mark_x += openfile->current_x;
 		}
+
+		/* Subtract 1 from any knots after the current line. */
+		slide_knots_after(openfile->current->lineno, -1, 0);
 #endif
 		unlink_node(joining);
 		renumber_from(openfile->current);
@@ -252,6 +255,9 @@ void extract_segment(linestruct *top, size_t top_x, linestruct *bot, size_t bot_
 
 	if (top == bot && top_x == bot_x)
 		return;
+
+	/* Adjust any knots that are after or in the cut piece. */
+	slide_knots_after(bot->lineno, top->lineno - bot->lineno, top->lineno);
 #endif
 
 	/* Reduce the buffer to cover just the text that needs to be extracted. */
@@ -325,6 +331,7 @@ void extract_segment(linestruct *top, size_t top_x, linestruct *bot, size_t bot_
  * at the current cursor position. */
 void ingraft_buffer(linestruct *topline)
 {
+	size_t was_lineno = openfile->current->lineno;
 	size_t current_x_save = openfile->current_x;
 	/* Remember whether the current line is at the top of the edit window. */
 	bool edittop_inside = (openfile->edittop == openfile->current);
@@ -342,8 +349,10 @@ void ingraft_buffer(linestruct *topline)
 	/* Replace the current buffer with the passed buffer. */
 	openfile->filetop = topline;
 	openfile->filebot = topline;
-	while (openfile->filebot->next != NULL)
+	while (openfile->filebot->next != NULL) {
 		openfile->filebot = openfile->filebot->next;
+		openfile->filebot->lineno = openfile->filebot->prev->lineno + 1;
+	}
 
 	/* Put the cursor at the end of the pasted text. */
 	openfile->current = openfile->filebot;
@@ -364,6 +373,9 @@ void ingraft_buffer(linestruct *topline)
 		} else
 			openfile->mark = openfile->filetop;
 	}
+
+	/* Adjust any knots that are after the grafting point. */
+	slide_knots_after(was_lineno, openfile->filebot->lineno - topline->lineno, 0);
 #endif
 
 	/* Add the number of characters in the copied text to the file size. */
diff --git a/src/files.c b/src/files.c
index cbeba2ed..f6624ade 100644
--- a/src/files.c
+++ b/src/files.c
@@ -87,6 +87,11 @@ void make_new_buffer(void)
 
 	openfile->fmt = NIX_FILE;
 
+	openfile->origin = nmalloc(sizeof(knotstruct));
+	openfile->origin->lineno = 0;
+	openfile->origin->prev = openfile->origin;
+	openfile->origin->next = openfile->origin;
+
 	openfile->undotop = NULL;
 	openfile->current_undo = NULL;
 	openfile->last_saved = NULL;
diff --git a/src/global.c b/src/global.c
index e8fa4629..e94255aa 100644
--- a/src/global.c
+++ b/src/global.c
@@ -670,6 +670,9 @@ void shortcut_init(void)
 #ifndef NANO_TINY
 	const char *recordmacro_gist = N_("Start/stop recording a macro");
 	const char *runmacro_gist = N_("Run the last recorded macro");
+	const char *knot_gist = N_("Place or remove a knot at the current line");
+	const char *upknot_gist = N_("Jump backward to the nearest knot");
+	const char *downknot_gist = N_("Jump forward to the nearest knot");
 #endif
 	const char *case_gist = N_("Toggle the case sensitivity of the search");
 	const char *reverse_gist = N_("Reverse the direction of the search");
@@ -1023,6 +1026,13 @@ void shortcut_init(void)
 	add_to_funcs(run_macro, MMAIN,
 		N_("Run Macro"), WITHORSANS(runmacro_gist), BLANKAFTER, VIEW);
 
+	add_to_funcs(make_knot, MMAIN,
+		N_("Knot"), WITHORSANS(knot_gist), TOGETHER, VIEW);
+	add_to_funcs(up_to_knot, MMAIN,
+		N_("Up to knot"), WITHORSANS(upknot_gist), TOGETHER, VIEW);
+	add_to_funcs(down_to_knot, MMAIN,
+		N_("Down to knot"), WITHORSANS(downknot_gist), BLANKAFTER, VIEW);
+
 	add_to_funcs(zap_text, MMAIN,
 		N_("Zap Text"), WITHORSANS(zap_gist), BLANKAFTER, NOVIEW);
 
@@ -1188,6 +1198,9 @@ void shortcut_init(void)
 	add_to_sclist(MMAIN, "Sh-Tab", SHIFT_TAB, do_unindent, 0);
 	add_to_sclist(MMAIN, "M-:", 0, record_macro, 0);
 	add_to_sclist(MMAIN, "M-;", 0, run_macro, 0);
+	add_to_sclist(MMAIN, "M-'", 0, make_knot, 0);
+	add_to_sclist(MMAIN, "M-PgUp", ALT_PAGEUP, up_to_knot, 0);
+	add_to_sclist(MMAIN, "M-PgDn", ALT_PAGEDOWN, down_to_knot, 0);
 	add_to_sclist(MMAIN, "M-U", 0, do_undo, 0);
 	add_to_sclist(MMAIN, "M-E", 0, do_redo, 0);
 	add_to_sclist(MMAIN, "Sh-^Del", CONTROL_SHIFT_DELETE, chop_previous_word, 0);
diff --git a/src/nano.h b/src/nano.h
index 2815afc1..812cfb3b 100644
--- a/src/nano.h
+++ b/src/nano.h
@@ -294,6 +294,12 @@ typedef struct linestruct {
 } linestruct;
 
 #ifndef NANO_TINY
+typedef struct knotstruct {
+	size_t lineno;
+	struct knotstruct *prev;
+	struct knotstruct *next;
+} knotstruct;
+
 typedef struct groupstruct {
 	ssize_t top_line;
 		/* First line of group. */
@@ -385,6 +391,8 @@ typedef struct openfilestruct {
 		/* The file's format -- Unix or DOS or Mac or mixed. */
 	char *lock_filename;
 		/* The path of the lockfile, if we created one. */
+	knotstruct *origin;
+		/* The base element of a circular list of knots (bookmarks). */
 	undostruct *undotop;
 		/* The top of the undo list. */
 	undostruct *current_undo;
diff --git a/src/proto.h b/src/proto.h
index 17c69c6b..dbbe594a 100644
--- a/src/proto.h
+++ b/src/proto.h
@@ -490,6 +490,10 @@ void do_find_bracket(void);
 /* Most functions in text.c. */
 #ifndef NANO_TINY
 void do_mark(void);
+void make_knot(void);
+void up_to_knot(void);
+void down_to_knot(void);
+void slide_knots_after(size_t threshold, int difference, size_t floor);
 #endif
 void do_tab(void);
 #ifndef NANO_TINY
diff --git a/src/text.c b/src/text.c
index 9137c8f7..68758ce6 100644
--- a/src/text.c
+++ b/src/text.c
@@ -57,6 +57,92 @@ void do_mark(void)
 		refresh_needed = TRUE;
 	}
 }
+
+/* Place a knot at the current line when none exists, otherwise remove it. */
+void make_knot(void)
+{
+	knotstruct *knot = openfile->origin->next;
+
+	while (knot->lineno < openfile->current->lineno && knot != openfile->origin)
+		knot = knot->next;
+
+	if (knot->lineno == openfile->current->lineno) {
+		knot->prev->next = knot->next;
+		knot->next->prev = knot->prev;
+		free(knot);
+		statusbar(_("Removed knot"));
+	} else {
+		knotstruct *newone = nmalloc(sizeof(knotstruct));
+
+		newone->next = knot;
+		newone->prev = knot->prev;
+		knot->prev->next = newone;
+		knot->prev = newone;
+
+		newone->lineno = openfile->current->lineno;
+
+		statusbar(_("Placed knot"));
+	}
+}
+
+/* Jump to the nearest knot, if there are any, either backwards or forwards. */
+void jump_to_nearest_knot(bool direction)
+{
+	knotstruct *knot = (direction == BACKWARD) ? openfile->origin->prev :
+												openfile->origin->next;
+
+	if (knot == openfile->origin) {
+		statusline(ALERT, _("There are no knots"));
+		return;
+	} else if (knot->prev == knot->next && knot->lineno == openfile->current->lineno) {
+		statusbar(_("This is the only knot"));
+		return;
+	}
+
+	if (direction == BACKWARD)
+		while (knot->lineno >= openfile->current->lineno && knot != openfile->origin)
+			knot = knot->prev;
+	else
+		while (knot->lineno <= openfile->current->lineno && knot != openfile->origin)
+			knot = knot->next;
+
+	if (knot == openfile->origin)
+		knot = (direction == BACKWARD) ? knot->prev : knot->next;
+
+	openfile->current = line_from_number(knot->lineno);
+	openfile->current_x = 0;
+	openfile->placewewant = 0;
+	refresh_needed = TRUE;
+}
+
+/* Jump to the first knot before the current line; wrap around at the top. */
+void up_to_knot(void)
+{
+	jump_to_nearest_knot(BACKWARD);
+}
+
+/* Jump to the first knot after the current line; wrap around at the bottom. */
+void down_to_knot(void)
+{
+	jump_to_nearest_knot(FORWARD);
+}
+
+/* Adjust any knots that are located after the given threshold -- adjust
+ * them by the given difference.  If floor is nonzero, then knots between
+ * floor and threshold should become equal to floor. */
+void slide_knots_after(size_t threshold, int difference, size_t floor)
+{
+	knotstruct *knot = openfile->origin->next;
+
+	while (knot != openfile->origin) {
+		if (knot->lineno > threshold)
+			knot->lineno += difference;
+		else if (floor && knot->lineno > floor)
+			knot->lineno = floor;
+
+		knot = knot->next;
+	}
+}
 #endif /* !NANO_TINY */
 
 /* Insert a tab.  If the TABS_TO_SPACES flag is set, insert the number
@@ -531,6 +617,7 @@ void do_undo(void)
 		strcat(line->data, &u->strdata[u->tail_x]);
 		unlink_node(line->next);
 		renumber_from(line);
+		slide_knots_after(line->lineno, -1, 0);
 		goto_line_posx(u->head_lineno, u->head_x);
 		break;
 	case BACK:
@@ -558,6 +645,7 @@ void do_undo(void)
 		intruder->data = copy_of(u->strdata);
 		splice_node(line, intruder);
 		renumber_from(intruder);
+		slide_knots_after(line->lineno, +1, 0);
 		goto_line_posx(u->head_lineno, u->head_x);
 		break;
 	case REPLACE:
@@ -874,6 +962,9 @@ void do_enter(void)
 		openfile->mark = newnode;
 		openfile->mark_x += extra - openfile->current_x;
 	}
+
+	/* Add 1 to any knots after the current line. */
+	slide_knots_after(openfile->current->lineno, +1, 0);
 #endif
 
 	/* Insert the newly created line after the current one and renumber. */
-- 
2.25.2

