Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions misc/haiku/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 0 additions & 8 deletions nnn.1
Original file line number Diff line number Diff line change
Expand Up @@ -292,14 +292,6 @@ 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
Expand Down
197 changes: 162 additions & 35 deletions src/nnn.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -598,8 +609,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[] = {
Expand Down Expand Up @@ -645,6 +657,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
Expand Down Expand Up @@ -817,6 +830,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);
Expand Down Expand Up @@ -1493,8 +1507,6 @@ static void startselection(void)
writesel(NULL, 0);
selbufpos = 0;
}

lastappendpos = 0;
}
}

Expand All @@ -1520,32 +1532,146 @@ static size_t appendslash(char *path)
return len;
}

static void invertselbuf(char *path, bool toggle)
static char *findinsel(int len)
{
selbufpos = lastappendpos;
if (!selbufpos)
return FALSE;

if (toggle || nselected) {
size_t len = appendslash(path);
char *found = 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, selbufpos - (found - pselbuf), g_buf, len);
if (!found)
return NULL;

for (int i = 0; i < ndents; ++i) {
if (toggle) { /* Toggle selection status */
pdents[i].flags ^= FILE_SELECTED;
pdents[i].flags & FILE_SELECTED ? ++nselected : --nselected;
}
if (found == pselbuf || *(found - 1) == '\0')
return found;

/* We found g_buf as a substring of a path, move forward */
found += len;
if (found >= pselbuf + selbufpos)
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));

/* 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(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;

/* 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;

if (len > 1)
--len;
path[len] = '\0';
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(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);
Expand Down Expand Up @@ -5139,9 +5265,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);

Expand All @@ -5157,9 +5284,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;
Expand Down Expand Up @@ -5199,6 +5323,15 @@ 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] = '/';
found = findinsel(off) != NULL;
Copy link
Owner

@jarun jarun Jul 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another scope for a good optimization here. If found is not NULL, we can remember the location at which we found the first occurrence of the path and do the subsequent searches from there instead of the start of the selection buffer, In the most common workflow all files in a directory will be selected together and so we can save a lot on the subsequent find()s because they will be found towards near this offset.

Copy link
Owner

@jarun jarun Jul 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may have to pass the address to start checking from to findinsel() for this. But I think that's an improvement. If NULL is passed, use selbuf, else use the passed address. The length of the memory to be scanned may also need to be adjusted as we have to discard the starting addr - selbuf bytes from total length of the buffer.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be done.

Copy link
Collaborator Author

@KlzXS KlzXS Jul 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately files in a directory are listed in an arbitrary order so we can only start from the first appearance of the dir. Too bad.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still better than beginning at the start of the selection buffer. 👍

} else
found = TRUE;

off = 0;

do {
namep = dp->d_name;

Expand Down Expand Up @@ -5340,6 +5473,9 @@ static int dentfill(char *path, struct entry **ppdents)
entflags = 0;
}

if (found && findinsel(mkpath(path, dentp->name, buf)) != NULL)
dentp->flags |= FILE_SELECTED;

if (cfg.blkorder) {
if (S_ISDIR(sb.st_mode)) {
mkpath(path, namep, buf);
Expand Down Expand Up @@ -5611,9 +5747,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;
Expand Down Expand Up @@ -6182,9 +6315,6 @@ static bool browse(char *ipath, const char *session, int pkey)
}
#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);
Expand Down Expand Up @@ -6267,9 +6397,6 @@ static bool browse(char *ipath, const char *session, int pkey)
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 */
Expand Down Expand Up @@ -6850,7 +6977,7 @@ static bool browse(char *ipath, const char *session, int pkey)
writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
} else {
--nselected;
invertselbuf(path, FALSE);
rmfromselbuf(mkpath(path, pdents[cur].name, g_buf));
}

#ifndef NOX11
Expand Down Expand Up @@ -6919,7 +7046,7 @@ static bool browse(char *ipath, const char *session, int pkey)
}

(sel == SEL_SELINV)
? invertselbuf(path, TRUE) : addtoselbuf(path, selstartid, selendid);
? invertselbuf(path) : addtoselbuf(path, selstartid, selendid);

#ifndef NOX11
if (cfg.x11)
Expand Down