‹ . local author .

: notes on place, literature, history, and method :

Emacs: With ace-window and link-hint, open links exactly where you want them

This post describes how the Emacs package link-hint can be combined with ace-window to allow you to select, on-the-fly, which window a link will open in—instead of letting fate, or custom, or Emacs decide for you.

——————————————————————————————

Ace-window is (more than) a fancy window-switcher

The ace-window package, written by abo-abo, offers a handy, full-featured alternative to the built-in window-switching function other-window. The package’s primary function—the titular ace-window—overlays a different character (letter or number) over each visible Emacs window, so you can switch to the desired window by simply pressing its assigned character. Very useful, very sleek.

But ace-window is more than just a handy way to switch windows.

The package also offers the ability to perform an action in the selected window before switching to it. In this regard, it follows the design of its parent package avy—also by abo-abo. Instead of simply switching to a window, you can split it first, call switch-to-buffer in it, or even close it. For a demonstration of this aspect of ace-window, see this video from Protesilaos Stavrou.)

The greatness of ace-window truly comes through, however, when it is combined with other packages.

With ace-window and Embark, open files/buffers in specified windows

In his detailed write-up on using Embark, Karthik Chikmagalur showcases a killer mash-up of ace-window and Embark, demonstrating how ace-window can be used for more than just switching windows.

Briefly put, Karthik shows how to incorporate ace-window’s window-selection function into the opening of a file or buffer from the minibuffer so that, as he writes, “any buffer/file/bookmark I open is always placed exactly where I want it to be on the screen.”

An example usage would be:

  1. call find-file
  2. call embark-act on file you wish to open
  3. call my/embark-ace-action-find-file (this is the slightly unwieldy name Karthik gives to the embark/ace-window function)
  4. select desired window with ace-window

Inspired by what Karthik showed was possible, I wanted to incorporate ace-window with link-hint, so that I could choose which window a link would open in. The result is a function called link-hint-aw-select:

Using ’link-hint-aw-select’ to open each of the links in the top left window in a different window. First, the desired link is chosen via character overlay, courtsey of link-hint, then the desired window is chosen via character overlay, courtsey of ace-window.

Using ’link-hint-aw-select’ to open each of the links in the top left window in a different window. First, the desired link is chosen via character overlay, courtsey of link-hint, then the desired window is chosen via character overlay, courtsey of ace-window.

If you’re unfamiliar, link-hint is a package that is conceptually similar to ace-window: it puts a character overlay on visible links in a buffer so that you can choose a link to follow by typing its assigned character. (The conceptual similarity of link-hint and ace-window is not surprising, since both are based on avy.)

Link-hint can identify a lot of different type of links, such as urls, file paths, mailto links, org-links, buttons, and dired filenames. New link types can also be defined by the user. (More on that later.)

As with avy and ace-window, link-hint allows for different actions to performed on the chosen link. However, there are only two default actions for acting on links: open link and copy link.

Defining a new action is done by defining a new function, in this case the above-mentioned link-hint-aw-select, which will be entry-point command for all link types:

(defun link-hint-aw-select ()
  "Use avy to open a link in a window selected with ace-window."
  (interactive)
  (unless
      (avy-with link-hint-aw-select
        (link-hint--one :aw-select))
    (message "No visible links")))

Things get more complicated very quickly, however.

Each action must be tailored to each different link type. Take the case of the “open” action, for examples: urls open one way—in a browser—and filepaths open another way—in a relevant application—and email addresses open another way—in a new draft email. Each action is actually many different actions, all roughly similar.

When creating a new action, then, it is necessary to make sure that each link type is associated with a function that will perform the new action in the appropriate way.

The aw-select action for file links is quite simple:

(defun link-hint--aw-select-file-link (link)
  (with-demoted-errors "%s"
    (aw-switch-to-window (aw-select nil))
    (find-file link)))

To make the link-hint ecosystem aware of this new association of link-type, action, and function, there is a handy macro:

(link-hint-define-type 'file-link
  :aw-select #'link-hint--aw-select-file-link)

If different link-types behave similarly, it is possible to create a macro of our own that will do all of the above very efficiently. This is the case with buttons and dired-filenames, for example:

(defmacro define-link-hint-aw-select (link-type fn)
  `(progn
     (link-hint-define-type ',link-type
       :aw-select #',(intern (concat "link-hint--aw-select-"
                                     (symbol-name link-type))))
     (defun ,(intern (concat "link-hint--aw-select-"
                             (symbol-name link-type))) (_link)
       (with-demoted-errors "%s"
         (if (> (length (aw-window-list)) 1)
             (let ((window (aw-select nil))
                   (buffer (current-buffer))
                   (new-buffer))
               (,fn)
               (setq new-buffer (current-buffer))
               (switch-to-buffer buffer)
               (aw-switch-to-window window)
               (switch-to-buffer new-buffer))
           (link-hint-open-link-at-point))))))

(define-link-hint-aw-select button push-button)
(define-link-hint-aw-select dired-filename dired-find-file)

The same is almost the case with org-links as well, except that by default org-links are opened using find-file-other-window instead of find-file, meaning that the above macro wouldn’t work properly.

I prefer to change this setting globally, so that org-links are opened in the current window, by evaluating the following:

(setf (cdr (assoc 'file org-link-frame-setup)) 'find-file)

This allows me to use the macro:

(define-link-hint-aw-select org-link org-open-at-point)

To leave org’s default behavior in place globally, it is necessary to define a function specifically for org-links, in which the behavior is changed locally in a let-binding:

(defun link-hint--aw-select-org-link (_link)
  (let ((org-link-frame-setup
         '((file . find-file))))
    (with-demoted-errors "%s"
      (if (> (length (aw-window-list)) 1)
          (let ((window (aw-select nil))
                (buffer (current-buffer))
                (new-buffer))
            (org-open-at-point)
            (setq new-buffer
                  (current-buffer))
            (switch-to-buffer buffer)
            (aw-switch-to-window window)
            (switch-to-buffer new-buffer))
        (link-hint-open-link-at-point)))))

(link-hint-define-type 'org-link :aw-select #'link-hint--aw-select-org-link)

Things are actually even a bit more complicated than this with org-links.

Since org handles different link-types internally, the function link-hint--aw-select-org-link can also be made to handle certain org link-types differently. For example, if you want http/https links to be opened externally, there is no reason to call the aw-select behavior for that type of org-link. This could be accomplished by including something like the following in the function’s “if” conditional:

(not (member (org-element-property :type (org-element-context))
             '("http" "https")))

The whole function would be:

(defun link-hint--aw-select-org-link (_link)
  (let ((org-link-frame-setup
         '((file . find-file))))
    (with-demoted-errors "%s"
      (if (and (> (length (aw-window-list)) 1)
               (not (member (org-element-property
                             :type (org-element-context))
                       '("http" "https"))))
          (let ((window (aw-select nil))
                (buffer (current-buffer))
                (new-buffer))
            (org-open-at-point)
            (setq new-buffer
                  (current-buffer))
            (switch-to-buffer buffer)
            (aw-switch-to-window window)
            (switch-to-buffer new-buffer))
        (link-hint-open-link-at-point)))))

Example use-case: note-taking, zettelkasten, etc.

I find this functionality really useful in my note-taking environment, where each of my notes contains several links to other notes. I rarely want to follow a link in the same window as the note where the link appears. I typically want to leave that note where it is and open a link somewhere else. If I have several windows arrayed around my screen, I can use link-hint-aw-select to open the linked note exactly where I want it to be.

To use this with org, the above configuration is enough. If you use another note-taking environment, you may need to define a new link type.

For example, for use with my own Zettelkasten package zk, I define a new link type zk-link and a function zk-link-hint-aw-select, along with other functions necessary to fully incorporate a new link type into the link-hint system:

(defun zk-link-hint--zk-link-at-point-p ()
  "Return the ID for the zk-link at the point or nil."
  (and (zk--id-at-point)
       (thing-at-point-looking-at zk-link-regexp)))

(defun zk-link-hint--next-zk-link (bound)
  "Find the unext zk-link.
Only search the range between just after the point and BOUND."
  (link-hint--next-regexp zk-id-regexp bound))

(defun link-hint--aw-select-zk-link (id)
  (with-demoted-errors "%s"
    (if (> (length (aw-window-list)) 1)
        (let ((window (aw-select nil))
              (buffer (current-buffer))
              (new-buffer))
          (zk-follow-link-at-point id)
          (setq new-buffer
                (current-buffer))
          (switch-to-buffer buffer)
          (aw-switch-to-window window)
          (switch-to-buffer new-buffer))
      (link-hint-open-link-at-point))))

(link-hint-define-type 'zk-link
  :next #'zk-link-hint--next-zk-link
  :at-point-p #'zk-link-hint--zk-link-at-point-p
  :open #'zk-follow-link-at-point
  :copy #'kill-new
  :aw-select #'link-hint--aw-select-zk-link)

(push 'link-hint-zk-link link-hint-types)

Even though link-hint can be a bit complex under the hood, it is quite effortless, even elegant, when put in combination with ace-window. Give it a try and puzzle over what you had done so long without it.