From: KlzXS <klzx+github@klzx.cf>
Date: Sat, 10 Jul 2021 01:49:39 +0000 (+0200)
Subject: Persistent selection (#1086)
X-Git-Tag: v4.2~34
X-Git-Url: http://www.git.stargrave.org/?a=commitdiff_plain;h=c0dceb18c687b8bd9cd9b429a66ca4da0ad8700e;p=nnn.git

Persistent selection (#1086)

* Add persistsel

* Fix Makefile spacing

* Update Haiku Makefile

* Do a double pass on inversion

* Split single and double pass for easier testing

Removed lastappendpos

Eliminate suffix matches

* Check if dir is in selection before searching for files

Fix double pass

* Switch to mainline

Optimize memory moving

Handle large selection in invertsel()

Going forward with 2pass

* Update Makefiles

* Fix style

* Move forward declarations

* Remove edit selection in inversion

Replace buf with g_buf to fix CI

Fix CI

* Style changes

* Comment the code

* Style fixes

* Fix infinite loop

* Fix crash on empty invert

* Fix off-by-one-in-two-places

Off-by-twice?

* Adopt changes from master

* Only check directory if entry in it is selected

* Better organization

* Wrong variable

* Tiny optimizations

* Style fixes and updated man page

* Update man page

* Remember where we found directory path in selection

Add in progress message on invert
---

diff --git a/Makefile b/Makefile
index 6595f61a..3f4eeb97 100644
--- a/Makefile
+++ b/Makefile
@@ -26,6 +26,7 @@ O_BENCH := 0  # benchmark mode (stops at first user input)
 O_NOSSN := 0  # enable session support
 O_NOUG := 0  # disable user, group name in status bar
 O_NOX11 := 0  # disable X11 integration
+O_LARGESEL := 0 # set threshold for large selection
 
 # User patches
 O_GITSTATUS := 0 # add git status to detail view
@@ -115,6 +116,10 @@ ifeq ($(strip $(O_NOX11)),1)
 	CPPFLAGS += -DNOX11
 endif
 
+ifneq ($(strip $(O_LARGESEL)),0)
+	CPPFLAGS += -DLARGESEL=$(strip $(O_LARGESEL))
+endif
+
 ifeq ($(shell $(PKG_CONFIG) ncursesw && echo 1),1)
 	CFLAGS_CURSES ?= $(shell $(PKG_CONFIG) --cflags ncursesw)
 	LDLIBS_CURSES ?= $(shell $(PKG_CONFIG) --libs   ncursesw)
diff --git a/misc/haiku/Makefile b/misc/haiku/Makefile
index c4aabe14..2da240a1 100644
--- a/misc/haiku/Makefile
+++ b/misc/haiku/Makefile
@@ -24,6 +24,7 @@ O_BENCH := 0  # benchmark mode (stops at first user input)
 O_NOSSN := 0  # enable session support
 O_NOUG := 0  # disable user, group name in status bar
 O_NOX11 := 0  # disable X11 integration
+O_LARGESEL := 0 # set threshold for large selection
 
 # User patches
 O_GITSTATUS := 0 # add git status to detail view
@@ -118,6 +119,10 @@ ifeq ($(strip $(O_NOX11)),1)
 	CPPFLAGS += -DNOX11
 endif
 
+ifneq ($(strip $(O_LARGESEL)),0)
+	CPPFLAGS += -DLARGESEL=$(strip $(O_LARGESEL))
+endif
+
 ifeq ($(shell $(PKG_CONFIG) ncursesw && echo 1),1)
 	CFLAGS_CURSES ?= $(shell $(PKG_CONFIG) --cflags ncursesw)
 	LDLIBS_CURSES ?= $(shell $(PKG_CONFIG) --libs   ncursesw)
diff --git a/nnn.1 b/nnn.1
index f6714c8d..adb15c61 100644
--- a/nnn.1
+++ b/nnn.1
@@ -292,19 +292,10 @@ use the selection in the other pane.
 clears the selection after an operation with the selection. Plugins are allowed
 to define the behaviour individually.
 .Pp
-.Nm
-doesn't match directory entries for selected files after a redraw or after the
-user navigates away from the directory. An attempt to do so will increase
-memory consumption and processing significantly as
-.Nm
-allows selection across directories. So the selection marks are cleared. The
-selection can still be edited in the same instance.
-.Pp
-To edit the selection use the _edit selection_ key. Use this key to remove a
-file from selection after you navigate away from its directory or to remove
-duplicates. Editing doesn't end the selection mode. You can add more files to
-the selection and edit the list again. If no file is selected in the current
-session, this option attempts to list the selection file.
+To edit the selection use the _edit selection_ key. Editing doesn't end the
+selection mode. You can add more files to the selection and edit the list again.
+If no file is selected in the current session, this option attempts to list the
+selection file.
 .Sh FIND AND LIST
 There are two ways to search and list:
 .Pp
diff --git a/src/nnn.c b/src/nnn.c
index 3c2fda94..4d9a60bb 100644
--- a/src/nnn.c
+++ b/src/nnn.c
@@ -198,6 +198,11 @@
 #define SED "sed"
 #endif
 
+/* Large selection threshold */
+#ifndef LARGESEL
+#define LARGESEL 1000
+#endif
+
 #define MIN_DISPLAY_COL (CTX_MAX * 2)
 #define ARCHIVE_CMD_LEN 16
 #define BLK_SHIFT_512   9
@@ -271,6 +276,12 @@ typedef struct entry {
 #endif
 } *pEntry;
 
+/* Selection marker */
+typedef struct {
+	char *startpos;
+	size_t len;
+} selmark;
+
 /* Key-value pairs from env */
 typedef struct {
 	int key;
@@ -408,7 +419,7 @@ static int nselected;
 #ifndef NOFIFO
 static int fifofd = -1;
 #endif
-static uint_t idletimeout, selbufpos, lastappendpos, selbuflen;
+static uint_t idletimeout, selbufpos, selbuflen;
 static ushort_t xlines, xcols;
 static ushort_t idle;
 static uchar_t maxbm, maxplug;
@@ -426,7 +437,7 @@ static char *selpath;
 static char *listpath;
 static char *listroot;
 static char *plgpath;
-static char *pnamebuf, *pselbuf;
+static char *pnamebuf, *pselbuf, *findselpos;
 static char *mark;
 #ifndef NOFIFO
 static char *fifopath;
@@ -600,8 +611,9 @@ static char * const utils[] = {
 #define MSG_RM_TMP       39
 #define MSG_INVALID_KEY  40
 #define MSG_NOCHANGE     41
+#define MSG_LARGESEL     42
 #ifndef DIR_LIMITED_SELECTION
-#define MSG_DIR_CHANGED  42 /* Must be the last entry */
+#define MSG_DIR_CHANGED  43 /* Must be the last entry */
 #endif
 
 static const char * const messages[] = {
@@ -647,6 +659,7 @@ static const char * const messages[] = {
 	"remove tmp file?",
 	"invalid key",
 	"unchanged",
+	"inversion may be slow, continue?",
 #ifndef DIR_LIMITED_SELECTION
 	"dir changed, range sel off", /* Must be the last entry */
 #endif
@@ -813,6 +826,7 @@ static void redraw(char *path);
 static int spawn(char *file, char *arg1, char *arg2, char *arg3, ushort_t flag);
 static void move_cursor(int target, int ignore_scrolloff);
 static char *load_input(int fd, const char *path);
+static int editselection(void);
 static int set_sort_flags(int r);
 #ifndef NOFIFO
 static void notify_fifo(bool force);
@@ -1492,8 +1506,6 @@ static void startselection(void)
 			writesel(NULL, 0);
 			selbufpos = 0;
 		}
-
-		lastappendpos = 0;
 	}
 }
 
@@ -1519,32 +1531,154 @@ static size_t appendslash(char *path)
 	return len;
 }
 
-static void invertselbuf(char *path, bool toggle)
+static char *findinsel(char *startpos, int len)
 {
-	selbufpos = lastappendpos;
+	if (!selbufpos)
+		return FALSE;
 
-	if (toggle || nselected) {
-		size_t len = appendslash(path);
+	if (!startpos)
+		startpos = pselbuf;
 
-		for (int i = 0; i < ndents; ++i) {
-			if (toggle) { /* Toggle selection status */
-				pdents[i].flags ^= FILE_SELECTED;
-				pdents[i].flags & FILE_SELECTED ? ++nselected : --nselected;
-			}
+	char *found = startpos;
+	size_t buflen = selbuflen - (startpos - pselbuf);
+
+	while (1) {
+		/*
+		 * memmem(3):
+		 * This function is not specified in POSIX.1, but is present on a number of other systems.
+		 */
+		found = memmem(found, buflen - (found - startpos), g_buf, len);
+		if (!found)
+			return NULL;
+
+		if (found == startpos || *(found - 1) == '\0')
+			return found;
+
+		/* We found g_buf as a substring of a path, move forward */
+		found += len;
+		if (found >= startpos + buflen)
+			return NULL;
+	}
+}
 
-			if (pdents[i].flags & FILE_SELECTED)
-				appendfpath(path,
-					len + xstrsncpy(path + len, pdents[i].name, PATH_MAX - len));
+static int markcmp(const void *va, const void *vb)
+{
+	const selmark *ma = (selmark*)va;
+	const selmark *mb = (selmark*)vb;
+
+	return ma->startpos - mb->startpos;
+}
+
+static void invertselbuf(char *path)
+{
+	/* This may be slow for large selection, ask for confirmation */
+	if (nselected > LARGESEL && !xconfirm(get_input(messages[MSG_LARGESEL])))
+		return;
+
+	size_t len, endpos, offset = 0;
+	char *found;
+	int nmarked = 0, prev = 0;
+	selmark *marked = malloc(nselected * sizeof(selmark));
+
+	printmsg("processing...");
+	refresh();
+
+	/* First pass: inversion */
+	for (int i = 0; i < ndents; ++i) {
+		 /* Toggle selection status */
+		pdents[i].flags ^= FILE_SELECTED;
+
+		/* Find where the files marked for deselection are in selection buffer */
+		if (!(pdents[i].flags & FILE_SELECTED)) {
+			len = mkpath(path, pdents[i].name, g_buf);
+			found = findinsel(findselpos, len);
+
+			marked[nmarked].startpos = found;
+			marked[nmarked].len = len;
+			++nmarked;
+
+			--nselected;
+			offset += len; /* buffer size adjustment */
+		} else
+			++nselected;
+	}
+
+	/*
+	 * Files marked for deselection could be found in arbitrary order.
+	 * Sort by appearance in selection buffer.
+	 * With entries sorted we can merge adjacent ones allowing us to
+	 * move them in a single go.
+	 */
+	qsort(marked, nmarked, sizeof(selmark), &markcmp);
+
+	/* Some files might be adjacent. Merge them into a single entry */
+	for (int i = 1; i < nmarked; ++i) {
+		if (marked[i].startpos == marked[prev].startpos + marked[prev].len)
+			marked[prev].len += marked[i].len;
+		else {
+			++prev;
+			marked[prev].startpos = marked[i].startpos;
+			marked[prev].len = marked[i].len;
 		}
+	}
+
+	/*
+	 * Number of entries is increased by encountering a non-adjacent entry
+	 * After we finish the loop we should increment it once more.
+	 */
+
+	if (nmarked) /* Make sure there is something to deselect */
+		nmarked = prev + 1;
 
-		if (len > 1)
-			--len;
-		path[len] = '\0';
+	/* Using merged entries remove unselected chunks from selection buffer */
+	for (int i = 0; i < nmarked; ++i) {
+		/*
+		 * found: points to where the current block starts
+		 *        variable is recycled from previous for readability
+		 * endpos: points to where the the next block starts
+		 *         area between the end of current block (found + len)
+		 *         and endpos is selected entries. This is what we are
+		 *         moving back.
+		 */
+		found = marked[i].startpos;
+		endpos = (i + 1 == nmarked ? selbufpos : marked[i + 1].startpos - pselbuf);
+		len = marked[i].len;
+
+		/* Move back only selected entries. No selected memory is moved twice */
+		memmove(found, found + len, endpos - (found + len - pselbuf));
+	}
+
+	/* Buffer size adjustment */
+	selbufpos -= offset;
+
+	free(marked);
+
+	/* Second pass: append newly selected to buffer */
+	for (int i = 0; i < ndents; ++i) {
+		/* Skip unselected */
+		if (!(pdents[i].flags & FILE_SELECTED))
+			continue;
+
+		len = mkpath(path, pdents[i].name, g_buf);
+		appendfpath(g_buf, len);
 	}
 
 	nselected ? writesel(pselbuf, selbufpos - 1) : clearselection();
 }
 
+/* removes g_buf from selbuf */
+static void rmfromselbuf(size_t len)
+{
+	char *found = findinsel(findselpos, len);
+	if (!found)
+		return;
+
+	memmove(found, found + len, selbufpos - (found + len - pselbuf));
+	selbufpos -= len;
+
+	nselected ? writesel(pselbuf, selbufpos - 1) : clearselection();
+}
+
 static void addtoselbuf(char *path, int startid, int endid)
 {
 	size_t len = appendslash(path);
@@ -5100,8 +5234,7 @@ static void dirwalk(char *dir, char *path, int entnum, bool mountpoint)
 	pthread_create(&tid, NULL, du_thread, (void *)&(core_data[core]));
 
 	redraw(dir);
-	tolastln();
-	addstr(" [^C aborts]\n");
+	printmsg("^C aborts");
 	refresh();
 }
 
@@ -5138,9 +5271,10 @@ static int dentfill(char *path, struct entry **ppdents)
 	uchar_t entflags = 0;
 	int flags = 0;
 	struct dirent *dp;
-	char *namep, *pnb, *buf = NULL;
+	bool found;
+	char *namep, *pnb, *buf = g_buf;
 	struct entry *dentp;
-	size_t off = 0, namebuflen = NAMEBUF_INCR;
+	size_t off, namebuflen = NAMEBUF_INCR;
 	struct stat sb_path, sb;
 	DIR *dirp = opendir(path);
 
@@ -5156,9 +5290,6 @@ static int dentfill(char *path, struct entry **ppdents)
 	if (cfg.blkorder) {
 		num_files = 0;
 		dir_blocks = 0;
-		buf = (char *)alloca(xstrlen(path) + NAME_MAX + 2);
-		if (!buf)
-			return 0;
 
 		if (fstatat(fd, path, &sb_path, 0) == -1)
 			goto exit;
@@ -5198,6 +5329,21 @@ static int dentfill(char *path, struct entry **ppdents)
 	}
 #endif
 
+	if (path[1]) { /* path should always be at least two bytes (including NULL) */
+		off = xstrsncpy(buf, path, PATH_MAX);
+		buf[off - 1] = '/';
+		/*
+		 * We set findselpos only here. Directories can be listed in arbitrary order.
+		 * This is the best best we can do for remembering position.
+		 */
+		found = (findselpos = findinsel(NULL, off)) != NULL;
+	} else {
+		findselpos = NULL;
+		found = TRUE;
+	}
+
+	off = 0;
+
 	do {
 		namep = dp->d_name;
 
@@ -5339,6 +5485,9 @@ static int dentfill(char *path, struct entry **ppdents)
 			entflags = 0;
 		}
 
+		if (found && findinsel(findselpos, mkpath(path, dentp->name, buf)) != NULL)
+			dentp->flags |= FILE_SELECTED;
+
 		if (cfg.blkorder) {
 			if (S_ISDIR(sb.st_mode)) {
 				mkpath(path, namep, buf);
@@ -5610,9 +5759,6 @@ static int handle_context_switch(enum action sel)
 			else
 				return -1;
 		}
-
-		if (g_state.selmode) /* Remember the position from where to continue selection */
-			lastappendpos = selbufpos;
 	}
 
 	return r;
@@ -6181,9 +6327,6 @@ begin:
 	}
 #endif
 
-	if (g_state.selmode && lastdir[0])
-		lastappendpos = selbufpos;
-
 #ifdef LINUX_INOTIFY
 	if ((presel == FILTER || watch) && inotify_wd >= 0) {
 		inotify_rm_watch(inotify_fd, inotify_wd);
@@ -6266,9 +6409,6 @@ nochange:
 				if (r >= CTX_MAX)
 					sel = SEL_BACK;
 				else if (r >= 0 && r != cfg.curctx) {
-					if (g_state.selmode)
-						lastappendpos = selbufpos;
-
 					savecurctx(path, pdents[cur].name, r);
 
 					/* Reset the pointers */
@@ -6849,7 +6989,7 @@ nochange:
 				writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
 			} else {
 				--nselected;
-				invertselbuf(path, FALSE);
+				rmfromselbuf(mkpath(path, pdents[cur].name, g_buf));
 			}
 
 #ifndef NOX11
@@ -6918,7 +7058,7 @@ nochange:
 			}
 
 			(sel == SEL_SELINV)
-				? invertselbuf(path, TRUE) : addtoselbuf(path, selstartid, selendid);
+				? invertselbuf(path) : addtoselbuf(path, selstartid, selendid);
 
 #ifndef NOX11
 			if (cfg.x11)