ericdanan / counsel-projectile

Ivy UI for Projectile
286 stars 39 forks source link

Sorting files by relevance to the query #167

Closed Gleek closed 4 years ago

Gleek commented 4 years ago

Hi, Thank you for the package. I was wondering if it was possible to sort candidates by relevance to the search query, i.e, move the files that are closer to the search query at the top when running counsel-projectile-find-file.

Setting (setq counsel-projectile-find-file-matcher 'counsel-projectile-find-file-matcher-basename) helps by prioritizing the files before sub-directory search but still the sorting order of the files where the basename matches could be improved, imho.

Let me explain by an example. Assuming this is the project:

.
├── a-somedirectory
│   ├── afile-suffix.txt
│   └── suffix.txt
├── suffix-directory
│   └── unrelated-file.txt
└── suffix.txt

and (setq counsel-projectile-find-file-matcher 'counsel-projectile-find-file-matcher-basename) has been executed.

Running counsel-projectile-find-file here with a string suffix gives these results:

a-somedirectory/afile-suffix.txt
a-somedirectory/suffix.txt
suffix.txt

In my opinion, the exact matches with the file root should be sorted on top. So the results should be

a-somedirectory/suffix.txt
suffix.txt
a-somedirectory/afile-suffix.txt

I'm not sure if such a thing is possible in the current ivy framework. The current ivy-sort-functions-alist does provide some sorting mechanism where two files can be compared, but I'm not aware if we could somehow also use the search query to determine the sort. Any pointers in the direction are appreciated. Thanks

Gleek commented 4 years ago

Hi, I've been able to hack a small solution using ivy-sort-matches-functions-alist. But I'm not sure how to apply it to the counsel-projectile-find-file function. Adding it to read-file-name-internal changes the order when running counsel-find-file to what I expect but doesn't affect counsel-projectile-find-file.

This is the idea which works loosely on the lines of how I want it to work

  (defvar +ivy-project-sort-min-length 1)
  (defun +ivy-project-sort--exact-match-file-base-name(name x y)
    (cond ((string= (file-name-nondirectory x) name) 1)
          ((string= (file-name-nondirectory y) name) 2)
          (t nil)))

  (defun +ivy-project-sort--exact-match-root-name(name x y)
    (cond ((string= (file-name-base x) name) 1)
          ((string= (file-name-base y) name) 2)
          (t nil)))

  (defun +ivy-project-sort--prefix-match-file-base-name(name x y)
    (cond ((string-match-p (concat "\\`" (funcall ivy--regex-function name)) (file-name-nondirectory x)) 1)
          ((string-match-p (concat "\\`" (funcall ivy--regex-function name)) (file-name-nondirectory y)) 2)
          (t nil)))

  (defun +ivy-project-sort--match-file-base-name(name x y)
    (cond ((string-match-p (regexp-quote name) (file-name-nondirectory x)) 1)
          ((string-match-p (regexp-quote name) (file-name-nondirectory y)) 2)
          (t nil)))

  (defun +ivy-project-sort-files(name candidates)
    "Assumes all candidates already match name"
    (if (>= (length name) +ivy-project-sort-min-length)
        (cl-sort (copy-sequence candidates)
                 (lambda (x y)
                   (if (eq (or (+ivy-project-sort--exact-match-file-base-name name x y)
                               (+ivy-project-sort--exact-match-root-name name x y)
                               (+ivy-project-sort--prefix-match-file-base-name name x y)
                               (+ivy-project-sort--match-file-base-name name x y)) 2) nil t)))
      candidates))
  ;; FIXME:
  ;; (add-to-list 'ivy-sort-matches-functions-alist '(read-file-name-internal . +ivy-project-sort-files))

Full config link

Any help in the modification of the ivy-sort-matches-functions-alist is appreciated.

Thanks

Gleek commented 4 years ago

Ah, seems like I had to write a custom matcher instead. Below is the final code in case anyone is interested. There's probably a better way to write this but this seems to works fine for me.

  (defvar +ivy-project-sort-min-length 1)
  (defun +ivy-project-sort--exact-match-file-base-name(name x y)
    (cond ((string= (file-name-nondirectory x) name) 1)
          ((string= (file-name-nondirectory y) name) 2)
          (t nil)))

  (defun +ivy-project-sort--exact-match-root-name(name x y)
    (cond ((string= (file-name-base x) name) 1)
          ((string= (file-name-base y) name) 2)
          (t nil)))

  (defun +ivy-project-sort--prefix-match-file-base-name(name x y)
    (cond ((string-match-p (concat "\\`" (funcall ivy--regex-function name)) (file-name-nondirectory x)) 1)
          ((string-match-p (concat "\\`" (funcall ivy--regex-function name)) (file-name-nondirectory y)) 2)
          (t nil)))

  (defun +ivy-project-sort--match-file-base-name(name x y)
    (cond ((string-match-p (regexp-quote name) (file-name-nondirectory x)) 1)
          ((string-match-p (regexp-quote name) (file-name-nondirectory y)) 2)
          (t nil)))

  (defun +ivy-project-sort-files(name candidates)
    "Assumes all candidates already match name"
    (if (>= (length name) +ivy-project-sort-min-length)
        (cl-sort (copy-sequence candidates)
                 (lambda (x y)
                   (if (eq (or (+ivy-project-sort--exact-match-file-base-name name x y)
                               (+ivy-project-sort--exact-match-root-name name x y)
                               (+ivy-project-sort--prefix-match-file-base-name name x y)
                               (+ivy-project-sort--match-file-base-name name x y)) 2) nil t)))
      candidates))
  (defun +counsel-projectile-find-file-matcher(regexp candidates)
    (+ivy-project-sort-files regexp (counsel--find-file-matcher regexp candidates)))
  (setq counsel-projectile-find-file-matcher '+counsel-projectile-find-file-matcher)

I have noticed very slight lag when I start typing on very large projects. Seems to run fine for average sized projects. Anyways, increasing the value of +ivy-project-sort-min-length should solve for the initial lag by removing any sort.

Closing this. Thank you for this package @ericdanan