;;; dirvish-narrow.el --- Live-narrowing of search results for Dirvish -*- lexical-binding: t -*-

;; Copyright (C) 2021-2025 Alex Lu
;; Author : Alex Lu <https://github.com/alexluigit>
;; Keywords: files, convenience
;; Homepage: https://github.com/alexluigit/dirvish
;; SPDX-License-Identifier: GPL-3.0-or-later

;;; Commentary:

;; This package provides live filtering of files in Dirvish buffers.  It is a
;; stripped-down version of `dired-narrow'.

;;; Code:

(require 'dirvish-fd)

;; Credit: copied from `orderless.el'
(defcustom dirvish-narrow-match-faces
  [dirvish-narrow-match-face-0
   dirvish-narrow-match-face-1
   dirvish-narrow-match-face-2
   dirvish-narrow-match-face-3]
  "Vector of faces used (cyclically) for component matches."
  :group 'dirvish :type '(vector face))

(defface dirvish-narrow-match-face-0
  '((default :weight bold)
    (((class color) (min-colors 88) (background dark)) :foreground "#72a4ff")
    (((class color) (min-colors 88) (background light)) :foreground "#223fbf")
    (t :foreground "blue"))
  "Face for matches of components numbered 0 mod 4."
  :group 'dirvish)

(defface dirvish-narrow-match-face-1
  '((default :weight bold)
    (((class color) (min-colors 88) (background dark)) :foreground "#ed92f8")
    (((class color) (min-colors 88) (background light)) :foreground "#8f0075")
    (t :foreground "magenta"))
  "Face for matches of components numbered 1 mod 4."
  :group 'dirvish)

(defface dirvish-narrow-match-face-2
  '((default :weight bold)
    (((class color) (min-colors 88) (background dark)) :foreground "#90d800")
    (((class color) (min-colors 88) (background light)) :foreground "#145a00")
    (t :foreground "green"))
  "Face for matches of components numbered 2 mod 4."
  :group 'dirvish)

(defface dirvish-narrow-match-face-3
  '((default :weight bold)
    (((class color) (min-colors 88) (background dark)) :foreground "#f0ce43")
    (((class color) (min-colors 88) (background light)) :foreground "#804000")
    (t :foreground "yellow"))
  "Face for matches of components numbered 3 mod 4."
  :group 'dirvish)

(defface dirvish-narrow-split
  '((t :inherit font-lock-negation-char-face))
  "Face used to highlight punctuation character."
  :group 'dirvish)

(defun dirvish-narrow--build-indices ()
  "Update the Dirvish buffer based on the input of the minibuffer."
  (save-excursion
    (cl-loop
     for (dir . beg) in dired-subdir-alist and idx from 0
     unless (and (eq idx 0) (dirvish-prop :fd-info))
     do (goto-char beg)
     (let ((end (dired-subdir-max)) (files (dirvish--ht)))
       (while (< (point) end)
         (when-let* ((f-beg (dired-move-to-filename))
                     (f-end (dired-move-to-end-of-filename))
                     (f-name (buffer-substring-no-properties f-beg f-end))
                     (l-beg (line-beginning-position))
                     (l-end (1+ (line-end-position)))
                     (l-str (buffer-substring l-beg l-end)))
           (puthash f-name l-str files))
         (forward-line 1))
       (puthash (md5 dir) files dirvish--dir-data)))))

(defun dirvish-narrow--compiler (s)
  "Compile `completion-regexp-list' from string S."
  (if (fboundp 'orderless-compile) (cdr (orderless-compile s)) (split-string s)))

(defun dirvish-narrow-update-h ()
  "Update the Dirvish buffer based on the input of the minibuffer."
  (let* ((mc (minibuffer-contents-no-properties))
         (filter mc) async rel igc)
    (save-match-data
      (when-let* (((string-match "^#\\([^ #]*\\)\\(.*\\)" mc))
                  (beg (minibuffer-prompt-end)))
        (add-text-properties beg (1+ beg) '(rear-nonsticky t))
        (add-face-text-property beg (1+ beg) 'dirvish-narrow-split)
        (setq async (match-string 1 mc) filter (match-string 2 mc))))
    (with-current-buffer (cdr (dv-index (dirvish-curr)))
      (when (and async (dirvish-prop :fd-info))
        (dirvish-fd--argparser (mapcan (lambda (x) `(,(format "--and=%s" x)))
                                       (split-string async "," t))
                               (cddr (dirvish-prop :fd-info))))
      (setq rel (dirvish-narrow--compiler filter)
            igc (cl-loop for re in (ensure-list rel)
                         always (isearch-no-upper-case-p re t)))
      (dirvish-prop :narrow-info (list async rel igc)))
    (dirvish--run-with-delay mc :narrow
      (lambda (_action)
        (with-current-buffer (cdr (dv-index (dirvish-curr)))
          (when (dirvish-prop :fd-info) (dirvish-fd--start-proc))
          (save-excursion
            (cl-loop for (dir . pos) in dired-subdir-alist and idx from 0
                     do (delete-region
                         (progn (goto-char pos)
                                (forward-line (dirvish--subdir-offset)) (point))
                         (- (dired-subdir-max) (if (eq idx 0) 0 1)))
                     unless (and (eq idx 0) (dirvish-prop :fd-info))
                     do (cl-loop with files = (gethash (md5 dir) dirvish--dir-data)
                                 with completion-regexp-list = rel
                                 with completion-ignore-case = igc
                                 for f in (all-completions "" files)
                                 do (insert (gethash f files))))))
        (when (dv-curr-layout (dirvish-curr)) (force-mode-line-update t))))))

(dirvish-define-attribute narrow-match
  "Highlight matched part of narrowed files."
  (cl-loop with (_ regexps case-fold-search) = (dirvish-prop :narrow-info)
           with n = (length dirvish-narrow-match-faces) with ovs = nil
           for regexp in regexps and i from 0
           when (string-match regexp f-str) do
           (cl-loop
            for (x y) on (let ((m (match-data))) (or (cddr m) m)) by #'cddr
            when x do (let ((ov (make-overlay (+ f-beg x) (+ f-beg y)))
                            (face (aref dirvish-narrow-match-faces (mod i n))))
                        (overlay-put ov 'face face)
                        (push ov ovs)))
           finally return `(ovs . ,ovs)))

;;;###autoload
(defun dirvish-narrow ()
  "Narrow a Dirvish buffer to the files matching a regex."
  (interactive nil dired-mode)
  (when (bound-and-true-p dirvish-subtree--overlays)
    (declare-function dirvish-subtree--revert "dirvish-subtree")
    (dirvish-subtree--revert t))
  (require 'orderless nil t)
  (dirvish-narrow--build-indices)
  (let ((dv (dirvish-prop :dv))
        (idx (dirvish-prop :index))
        (fd (dirvish-prop :fd-info))
        (attrs (mapcar #'car (dirvish-prop :attrs)))
        buffer-read-only)
    (when fd
      (setq dired-subdir-alist (list (car (reverse dired-subdir-alist))))
      (delete-region (goto-char (dirvish-prop :content-begin)) (point-max)))
    (dirvish-prop :attrs
      (dirvish--attrs-expand (append '(narrow-match) attrs)))
    (minibuffer-with-setup-hook
        (lambda ()
          (dirvish-prop :dv dv)
          (add-hook 'post-command-hook #'dirvish-narrow-update-h nil t))
      (unwind-protect
          (read-from-minibuffer "Focus on files: " (if fd "#" ""))
        (when idx (dired-goto-file idx))
        (dirvish-prop :attrs (dirvish--attrs-expand attrs))
        (when-let* (((not (eq (dv-type (dirvish-curr)) 'side)))
                    (query (caar (dirvish-prop :fd-info)))
                    (key (file-name-nondirectory
                          (directory-file-name default-directory))))
          (rename-buffer (concat key "🔍" query "🔍" (dv-id (dirvish-curr)))))
        (dirvish--run-with-delay 'reset)
        (dirvish--run-with-delay 'reset :narrow)))))

(provide 'dirvish-narrow)
;;; dirvish-narrow.el ends here
