Search code examples
emacselispbookmarks

Emacs quicker bookmark-jump?


I have most of my bookmarks prefixed by a letter in a way that the first letter almost always uniquely determines the bookmark. This way I can, for instance, jump to my source folder (bookmarked as "s: source") with M-x bookmark-jump RET s RET. I have it on a shortcut, so it's actually ~ s RET.

I'd like to get rid of RET in the end, i.e. get M-x bookmark-quick-jump RET s or ~ s to do the aforementioned job. I'd also like it to fall back to the default behavior: to show me all bookmarks that start with the given letter, in case there's not just one variant.

So far, I've got:

(defun bookmark-do-quick-jump (str)
  (let ((completions (all-completions str bookmark-alist)))
    (bookmark-jump
     (if (eq 1 (length completions))
         (car completions)
       (completing-read "Jump to bookmark: " bookmark-alist nil t str)))))

There's still two hiccups:

Firstly, I need to jump into minibuffer somehow and stick in there this map (don't know how to do this):

(setq bookmark-quick-jump-map
      (let ((map (make-sparse-keymap)))
        (mapcar (lambda (key) 
                  (define-key map key 
                    (lambda()
                      (interactive)
                      (bookmark-do-quick-jump key))))
                (loop for c from ?a to ?z
                      collect (string c)))
        map))

Secondly, when I do a call

(bookmark-do-quick-jump "o")

It comes back with 3 variants (org-capture-last-stored, org-capture-last-stored-marker...). I'm in minibuffer now, but I still need to press RET RET to see these 3 variants. I'd like this to be done automatically.

I'd appreciate any responses that either directly answer my two sub-problems, or an altogether different approach, as long as I can get the behavior and usability that I described.

UPD:

I've solved the second thing by switching from completing-read to ido-completing-read:

(defun bookmark-do-quick-jump (str)
  (let ((completions (all-completions str bookmark-alist)))
    (bookmark-jump
     (if (eq 1 (length completions))
         (car completions)
       (ido-completing-read "Jump to bookmark: " completions nil t str)))))

Btw, I forgot to mention that I use bookmark+. I'm not sure if jumping to dired is supported by the default bookmark-jump.


Solution

  • We can remap self-insert-command during the completing-read to trigger the auto-completion and auto-acceptance behaviour.

    I originally used (or (minibuffer-complete-and-exit) (minibuffer-completion-help)) which at first glance worked very nicely but, as noted in the comments, is less than ideal when one bookmark's name is the prefix of another, as it will immediately accept the shorter name, hence making the longer one inaccessible.

    Calling minibuffer-complete and minibuffer-completion-help together breaks the completion functionality, however, so instead I've copied the relevant part of minibuffer-complete-and-exit to a new function. Using this resolves all of the earlier problems.

    (require 'bookmark)
    
    (defvar bookmark-do-quick-jump-map (copy-keymap minibuffer-local-must-match-map)
      "Keymap for `bookmark-do-quick-jump'.
    
    `minibuffer-local-must-match-map' is used by `completing-read' when its
    REQUIRE-MATCH argument is t.
    
    In `bookmark-do-quick-jump' we bind this modified copy to use in its place.")
    
    (define-key bookmark-do-quick-jump-map
      [remap self-insert-command] 'my-self-insert-complete-and-exit)
    
    (defun bookmark-do-quick-jump ()
      "Jump to specified bookmark with auto-completion and auto-acceptance."
      (interactive)
      (bookmark-maybe-load-default-file)
      (let ((minibuffer-local-must-match-map bookmark-do-quick-jump-map))
        (bookmark-jump
         (completing-read "Jump to bookmark: " bookmark-alist nil t))))
    
    (defun my-self-insert-complete-and-exit (n)
      "Insert the character, then attempt to complete the current string,
    automatically exiting when only one option remains, and displaying the
    completion options otherwise."
      (interactive "p")
      (self-insert-command n)
      (my-minibuffer-complete)
      (let ((my-completions (completion-all-sorted-completions)))
        (if (and my-completions (eq 0 (cdr my-completions)))
            (exit-minibuffer)
          (minibuffer-completion-help))))
    
    (defun my-minibuffer-complete ()
      "Copied from `minibuffer-complete-and-exit'."
      (interactive)
      (condition-case nil
          (completion--do-completion nil 'expect-exact)
        (error 1)))
    

    Edit:

    I took another stab at this using ido. It's a little unfortunate that you don't get the next 'important character' highlighted the way that you do with the regular minibuffer completion (as that was a nice indicator of what to type next), but this seems to work nicely in other respects.

    (require 'bookmark)
    (require 'ido)
    
    (defvar bookmark-ido-quick-jump-map (copy-keymap minibuffer-local-map)
      "Keymap for `bookmark-ido-quick-jump'.
    
    Every time `ido-completing-read' is called it re-initializes
    `ido-common-completion-map' and sets its parent to be `minibuffer-local-map'.
    
    In `bookmark-ido-quick-jump' we provide this modified copy as a replacement
    parent.")
    
    (define-key bookmark-ido-quick-jump-map
      [remap self-insert-command] 'my-self-insert-and-ido-complete)
    
    (defun bookmark-ido-quick-jump ()
      "Jump to selected bookmark, using auto-completion and auto-acceptance."
      (interactive)
      (bookmark-maybe-load-default-file)
      (let ((minibuffer-local-map bookmark-ido-quick-jump-map)
            (ido-enable-prefix t))
        (bookmark-jump
         (ido-completing-read "Jump to bookmark: " 
                              (loop for b in bookmark-alist collect (car b))))))
    
    (defun my-self-insert-and-ido-complete (n)
      "Insert the character, then attempt to complete the current string,
    automatically exiting when only one option remains."
      (interactive "p")
      (self-insert-command n)
      ;; ido uses buffer-local pre- and post-command hooks, so we need to
      ;; co-operate with those. We append our post-command function so that
      ;; it executes after ido has finished processing our self-insert.
      (add-hook 'post-command-hook
                'my-self-insert-and-ido-complete-post-command t t))
    
    (defun my-self-insert-and-ido-complete-post-command ()
      (remove-hook 'post-command-hook
                   'my-self-insert-and-ido-complete-post-command t)
      ;; Now that ido has finished its normal processing for the current
      ;; command, we simulate a subsequent `ido-complete' command.
      (ido-tidy) ;; pre-command-hook
      (ido-complete)
      (ido-exhibit)) ;; post-command-hook