oantolin / orderless

Emacs completion style that matches multiple regexps in any order
GNU General Public License v3.0
771 stars 27 forks source link

Get file basename? #137

Closed rickalex21 closed 1 year ago

rickalex21 commented 1 year ago

Hello, how do I complete on file basenames only? I'm using vertico and consult-recent-file.

When I type 'a' I don't want a path with emacs to come up, only agenda.org because that's the basename of the file.

~/.config/emacs/init.el
~/.config/emacs/early-init.el
~/.config/emacs/org/agenda.org

I have changed this part of the code to initialism and it does not work with initialism. I would like to complete with the file basename but if it doesn't work with initialism it's probably not going to work with anything else. When I use the comma it does work because I have the variable +orderless-dispatch-alist.

completion-category-overrides '((file  (styles +orderless-with-initialism)) 

Here is the full config. I've configured most packages myself but this is one config that I've copied and pasted. This seems to be everywhere on the internet.

(use-package orderless
  :straight t
  :demand t
  ;;       orderless-component-separator "[ &]")
  ;; ...otherwise find-file gets different highlighting than other commands
  ;; (set-face-attribute 'completions-first-difference nil :inherit nil)
  :config
  (defvar +orderless-dispatch-alist
    '((?% . char-fold-to-regexp)
      (?! . orderless-without-literal)
      ; Same as \<, EG: ,c will search ~/.'c'onfig but not ~/Do'c'uments. Beginning of word.
      (?,. orderless-initialism)
      (?= . orderless-literal)
      (?~ . orderless-flex)))

  (defun +orderless-dispatch (pattern index _total)
    (cond
     ;; Ensure that $ works with Consult commands, which add disambiguation suffixes
     ((string-suffix-p "$" pattern)
      `(orderless-regexp . ,(concat (substring pattern 0 -1) "[\x100000-\x10FFFD]*$")))
     ;; File extensions
     ((and
       ;; Completing filename or eshell
       (or minibuffer-completing-file-name
           (derived-mode-p 'eshell-mode))
       ;; File extension
       (string-match-p "\\`\\.." pattern))
      `(orderless-regexp . ,(concat "\\." (substring pattern 1) "[\x100000-\x10FFFD]*$")))
     ;; Ignore single !
     ((string= "!" pattern) `(orderless-literal . ""))
     ;; Prefix and suffix
     ((if-let (x (assq (aref pattern 0) +orderless-dispatch-alist))
          (cons (cdr x) (substring pattern 1))
        (when-let (x (assq (aref pattern (1- (length pattern))) +orderless-dispatch-alist))
          (cons (cdr x) (substring pattern 0 -1)))))))

  ;; Define orderless style with initialism by default
  (orderless-define-completion-style +orderless-with-initialism
                                     (orderless-matching-styles '(orderless-initialism orderless-literal orderless-regexp)))

  (setq completion-styles '(orderless partial-completion orderless-initialism)
        completion-category-defaults nil
        ;;completion-category-overrides '((file (styles partial-completion)) ;; partial-completion is tried first
        completion-category-overrides '((file  (styles +orderless-with-initialism)) ;; partial-completion is tried first
                                        (command (styles +orderless-with-initialism))
                                        (consult-location (styles +orderless-with-initialism))
                                        (consult-multi (styles +orderless-with-initialism))
                                        (variable (styles +orderless-with-initialism))
                                        (symbol (styles +orderless-with-initialism)))
        orderless-component-separator #'orderless-escapable-split-on-space ;; allow escaping space with backslash!
        orderless-style-dispatchers '(+orderless-dispatch))
  )

Thanks

oantolin commented 1 year ago

It sounds like you want a matching style that appends [^/]+$ to a component and then matches it as a regexp. Let me know if that is enough of a hint or if you need help implementing it.

rickalex21 commented 1 year ago

Thanks @oantolin , I am aware that I can do regex in the minibuffer. I'm not sure how to do this automatically. I was under the impression that completion-category-overrides file styles would help me with this? Sorry, I still consider myself a newbie. I find embark easier to understand.

oantolin commented 1 year ago

Well, you have to decide exactly what kind of matching you want to support, but for example say you only want to match text literally and only against basenames. Then given a string like a.b what regexp do you actually want to match against the filepath? That would be a\.b[^/]*$ (the dot is escaped to make the dot match literally). A function that converts an input string into the regexp you actually want to match against is called a "matching style" in orderless, and this particular matching style would be defined as the function +match-basename below:

(defun +match-basename-literally (string)
  (format "\\(%s\\)[^/]*\\'" (regexp-quote string)))

(orderless-define-completion-style +orderless-basename-only
  "Completion style for matching against base file names only."
  (orderless-matching-styles '(+match-basename-literally)))

(setq completion-category-overrides
      '((project-file (styles +orderless-basename-only))
        (file (styles +orderless-basename-only))))

The rest of that code block defines a new completion style, +orderless-basename-only that exclusively uses this style of matching text literally and then the completion-category-overrides variable is set so that that completion style is tried first when completing either files or project-files (which also tend to have long paths).

You could add support for other styles of matching against basenames, like, maybe you want to match regexps against basenames, or maybe shell-style file globs (which is easy to implement thanks to the dired-glob-regexp function which turns a file glob into an equivalent regexp! —Emacs Lisp has one of the most ridiculously complete "standard libraries" of any language I've used).

rickalex21 commented 1 year ago

@oantolin This makes more sense, however I'm not getting the expected results. Is there something wrong with my config here? I updated it. I also comment out orderless-style-dispatchers just to be on the safe side.

As far as I'm aware all of these steps are done.

  1. Define function : +match-basename-literally converts input to a regex.
  2. Define style + Add func : +orderless-basename-only with +match-basename-literally
  3. Override Categories : Add cons (file (styles +orderless-basename-only)) to completion-category-overrides
(orderless-define-completion-style +orderless-with-initialism
  (orderless-matching-styles '(orderless-initialism
                              orderless-literal
                              orderless-regexp)))

(defun +match-basename-literally (string)
    (format "\\$%s\$[^/]\*\\\\'" (regexp-quote string)))

(orderless-define-completion-style +orderless-basename-only
  "Completion style for matching against base file names only."
  (orderless-matching-styles '(+match-basename-literally)))

(setq completion-styles '(orderless basic)
      completion-category-defaults nil
      completion-category-overrides '((file         (styles +orderless-basename-only))
                                      (project-file (styles +orderless-basename-only))
                                      ;;(file (styles partial-completion)) ;; partial-completion is tried first
                                      (command      (styles +orderless-with-initialism))
                                      (variable     (styles +orderless-with-initialism))
                                      (symbol       (styles +orderless-with-initialism)))
      orderless-component-separator #'orderless-escapable-split-on-space ;; allow escaping space with backslash!
      ;; Comment out for now till I get +orderless-basename-only working
      ;;orderless-style-dispatchers '(+orderless-dispatch)
      )

I'm not matching agenda-org.el, I'm matching emacs.org and others. Your regex works, I tested it in the picture. I imagine the single quote at the end of the regex is a suffix to match literately?

How do I know if something is explicitly file,command, variable, X, Y, Z etc..? I need to know this info for completion-category-overrides. I've seen other configs that have consult-something in there. I did read the part in the docs that talk about Marginalia which I have installed but I'm not so sure. Obviously this picture is file because I can see the file permissions. Obviously if I do Describe variable I know it's a variable. command I presume is M-x.

Thanks

oantolin commented 1 year ago

You changed the regexp! In my function I used "\\(%s\\)[^/]*\\'" and you used instead the very different "\\$%s\$[^/]\*\\\\'"! You were getting the correct results for the (weird) regexp you chose.

oantolin commented 1 year ago

I imagine the single quote at the end of the regex is a suffix to match literately?

No, \' matches the end of the string, just like $ matches the end of a line.

rickalex21 commented 1 year ago

@oantolin Sorry about that, it's the markdown exporter that I'm using, it's not my code. It needs to be fixed. Here is my code again without using the markdown exporter. As shown in the picture above I'm not matching agenda-org.el.

;; FIXME: Markdown exporter adding backslashes
(orderless-define-completion-style +orderless-with-initialism
  (orderless-matching-styles '(orderless-initialism
                              orderless-literal
                              orderless-regexp)))

(defun +match-basename-literally (string)
    (format "\\(%s\\)[^/]*\\'" (regexp-quote string)))

(orderless-define-completion-style +orderless-basename-only
  "Completion style for matching against base file names only."
  (orderless-matching-styles '(+match-basename-literally)))

(setq completion-styles '(orderless basic)
      completion-category-defaults nil
      completion-category-overrides '((file         (styles +orderless-basename-only))
                                      (project-file (styles +orderless-basename-only))
                                      ;;(file (styles partial-completion)) ;; partial-completion is tried first
                                      (command      (styles +orderless-with-initialism))
                                      (variable     (styles +orderless-with-initialism))
                                      (symbol       (styles +orderless-with-initialism)))
      orderless-component-separator #'orderless-escapable-split-on-space ;; allow escaping space with backslash!
      ;; Comment out for now till I get +orderless-basename-only working
      ;;orderless-style-dispatchers '(+orderless-dispatch)
      )
oantolin commented 1 year ago

As shown in the picture above I'm not matching agenda-org.el.

I'm confused: the picture clearly shows that agenda-org.el does match. Maybe instead of saying that agenda-org.el doesn't match you meant to say that you want the other files shown to not match?

If that is what you meant, one way to achieve that would be to anchor the regexps to the beginning of the basename. You could do that by changing the regexp from "\\(%s\\)[^/]*\\'" to "\\(?:^\\|/\\)\\(%s\\)[^/]*\\'".

Is that what you wanted?

rickalex21 commented 1 year ago

Yes ! Thanks alot Omar, that's what I needed. Hopefully this post will help someone else along the way. I understand Orderless a bit better now.

BTW

How do I know what category something is in: file,command, variable, X, Y, Z etc..? I need to know this info for completion-category-overrides. I've seen other configs that have consult-something in there. I did read the part in the docs that talk about Marginalia which I have installed but I'm not so sure. Obviously the picture above is file because I can see the file permissions. Obviously if I do Describe variable I know it's a variable and command I presume is M-x.

oantolin commented 1 year ago

How do I know what category something is in?

I wish Emacs made that a little easier than it is. I'd suggest you bind the following command to some key in minibuffer-local-map, then when you are in the minibuffer you can use the command to find out the category.

(defun show-completion-category ()
  (interactive)
  (message "Category: %s"
           (alist-get 'category
                      (cdr
                       (completion-metadata
                        (buffer-substring-no-properties
                         (minibuffer-prompt-end)
                         (max (minibuffer-prompt-end) (point)))
                        minibuffer-completion-table
                        minibuffer-completion-predicate)))))

(keymap-set minibuffer-local-map "M-?" #'show-completion-category)
rickalex21 commented 1 year ago

I see, thanks for the function. Is there something I need to autoload or do to use keymap-set ? I understand that define-key is legacy now. It's really weird cause I get void function keymap-set before doom-mode-line but after it it works fine. There's something in doom-modeline that enables keymap-set or emacs loads it at that time.

I even tried (autoload 'keymap-set "compat-29"), don't work. I get:

Symbol's function definition is void: keymap-set

BTW I'm using: GNU Emacs 28.2 (build 2, x86_64-unknown-linux-gnu, GTK+ Version 3.24.34, cairo version 1.16.0) of 2022-09-13

I don't have anything weird in my doom-modeline config:

;; Don't work before
;; (keymap-set minibuffer-local-map "C-c o s" #'oantolin/show-completion-category)

(use-package doom-modeline
  :straight t
  :demand t
  :custom-face
  (doom-modeline-bar-inactive ((t (:background "dim gray"))))
  (doom-modeline-buffer-modified ((t (:inherit (error bold) ))))
  (doom-modeline-lsp-error ((t (:inherit error :foreground "dark red" :weight normal))))
  (doom-modeline-urgent ((t (:inherit (error bold) :foreground "orange red"))))
  (doom-modeline-notification ((t (:foreground "dark orange" :background "black"))))
  (mode-line ((t (:family "Noto Sans" :height 140 ))))
  (mode-line-active ((t (:inherit (doom-modeline-project-parent-dir) ))))
  (mode-line-inactive ((t (:inherit (doom-modeline-project-parent-dir)))))
  :config
  (setq doom-modeline-project-detection 'auto)
  (setq doom-modeline-minor-modes t)
  (setq doom-modeline-lsp t)
  ;; (setq doom-modeline-buffer-file-name-style 'truncate-upto-root)
  (setq doom-modeline-buffer-file-name-style 'truncate-upto-project)
  :init (doom-modeline-mode 1))

;; Works after
(keymap-set minibuffer-local-map "C-c o s" #'oantolin/show-completion-category)
oantolin commented 1 year ago

You can easily rewrite that line to use define-key or general or whatever Doom uses to define key bindings instead of keymap-set, if you want.

Alternatively load compat which has a definition of keymap-set. Consult, for example, depends o compat.

rickalex21 commented 1 year ago

I use vanilla emacs, I had to install compat, that fixed it. Turns out it's required by doom-modeline.

UPDATE: I noticed the aqua color is the second match after space. Perhaps I misunderstood the way that the faces work but what I describe below would be a nice feature if it does not already exist.

I created a test function to use with your function. Are the matches suppose to be colored differently based on the style? They look the same color. ode matches my/orderless-test and org.org is matched by +match-basename-literally.

If this is the case then the first match ode should be color mangeta orderless-match-face-0 and the second match org.org should be aqua color orderless-match-face-1 ? However, everything looks the same color.

Not sure I understood this part of the readme.

You will only see these particular faces when the orderless completion is the one that ends up being used, of course.

These are the faces:

 (orderless-match-face-0 ((t (:foreground "#d33682" :background "#073642"))))
 (orderless-match-face-1 ((t (:foreground "#2aa198" :background "#073642"))))
 (orderless-match-face-2 ((t (:foreground "#b58900" :background "#073642"))))
 (orderless-match-face-3 ((t (:foreground "#859900" :background "#073642"))))

The code of interest:

(defun my/orderless-test (string)
(format "\\(%sd\\)\\([a-z]+\\)" (regexp-quote string)))

(orderless-define-completion-style my/path-with-basename
  (orderless-matching-styles '( +match-basename-literally my/orderless-test)))

oantolin commented 1 year ago

Are the matches suppose to be colored differently based on the style?

No, they are colored by component. So if your minibuffer input is ab cd then the matches of ab get one color and the matches of cd get a different color. For each component, each matching style is tried in order and the first matching style that makes that component match is used; but the color is the one that correspond to that number of component, not depending on the style that matched.

rickalex21 commented 1 year ago

Would it be difficult to implement something like that? If I'm matching +orderless-with-initialism color it yellow, if I'm matching +orderless-other-style color it green? Not sure what use cases but interesting for sure.

oantolin commented 1 year ago

It would not be too difficult, but I don't think it would be useful. Nor has anyone ever suggested it before. The point of highlighting the matches of different components of the input string in different colors is that they can match out of order (this is orderless after all), so the color helps you see were each component matched.

oantolin commented 1 year ago

I also think coloring based on matching style would be a little confusing with style dispatchers in the mix: people use style dispatchers to invent "query syntax" by translating input to appropriate string & matching-style pairs. The matching style used is really an implementation detail, and very often is just orderless-regexp. Is it helpful to highlight all matches that are implement via orderless-regexp in the color assigned to orderless-regexp? Probably not. The current system, where the color indicates the component number (mod 4) seems more informative to me.

minad commented 1 year ago

In order to get the completion category one can also use marginalia-cycle which prints a message.

rickalex21 commented 1 year ago

@oantolin That makes sense, thanks for all your help. Thanks for the tip @minad.