Metacircularism

Can You Do This in Vi?

Posted in Uncategorized by hayeah on 05.30.2008

Here’s a hack possible only with dynamic scoping. It’s ugly, but it works. It’s Emacs.

I use interactive search to navigate my source file. I might see piece of a text, and instead of up-up-up-up-left-left-left-left-left-up, I just search and jump directly to the spot. I’ve bound the search keys to

C-f C-j
C-f C-l

They coincide with my movement keys,

      C-i
C-j   C-k   C-l

So there’s a nice directionality in my muscle memory. Searching left, or searching left, are spatially mapped. Interactive search is a surprising powerful way to move around. Using movement keys, you have to constantly ask yourself, “am I there yet?”, “am I there yet, now?”, “where will we get there”, and your conscious mind gets angry and wants to throw the kids out of the window.

If I want to go back to “muscle memory” in the previous paragraph, I type,

C-f C-j “muscle memory”

Interactive search highlights the matches when I’ve typed “mus”. Seeing there are two matches, I use C-a and C-z to move backward and forward through the matches. So all in all, I’ve typed

C-f C-j “mus” C-a

This is unthinking in practice. My visual system only has to check,

(1) Is the place I want to go highlighted by isearch.
(2) Is my cursor there yet?

It’s just visual reflex. I don’t have to plan my way to the point I want to get to.

This is all very nice, what’s the hack I was talking about? Sometimes there are too many matches in the buffer. Suppose I want to get to “a nice directionality”, and I naively started search with the string,

C-f C-j “a”

The buffer lights up like a Christmas tree. There are too many matches for ‘a’, so it’s inpractical to jump to the place I want to go. If I keep typing to extend the search string, though, isearch eventually narrows down to my target. Eventually.

That’s not good enough. I’d like a way to say, I want to get to ‘a’, but only matches such that after it I could see the string “directionality.” So I want a way to perform context-sensitive search, with additional contexts provided incrementally should the need arise.

I defined a set of advises for isearch to add to the current search string with contexts. I’ve bound to M-j and M-l to add context to the left or to the right of my target.

So now, I could go to the place by typing something like

C-f C-j “a”

hmmm, too many matches, let’s add context

M-l “dir”

ahh! only three matches. C-a C-a C-a.

Again, I don’t need think how to get there. In my mind, I know where I’d like to go, and I tell Emacs /that’s/ my destination. Imagine you are taking a Taxi, you tell the driver where you’d like to go. Only very rarely you tell the driver “turn left”, “turn right”, “turn right”, “go straight for 5 blocks”, “turn left” THE WHOLE TRIP!

Context sensitive search turned out to be a very nice way to jump around in a source file when you don’t even know where the target is, but you know it’s there. For example, I may have a global variable ‘foo’. I’d like to go to where it’s defined. I know it’s at the top of my file, so I do a backward search.

C-j “defvar”

Oops, too many matches. Narrow by adding the variable name as rightward context,

M-l “foo”

I am there.

I can extend the context indefinitely. To find my keybinding for ‘icicle-candidate-set-union’, I might type

C-l “ici”

Too many,

M-j “kbd”

So the context now looks like

(kbd …) … ici…

Still to many,

M-l “union”

Ah! Perfect match. Here’s the hack. It’s funny how I have different coding standards for different linguistic communities. In Erlang, I undergo a visceral sickness every time I use “put”, a per process global variable sort of thing. In Emacs, “defadvice” is not encouraged, nor is it frowned upon. It’s there if you want/need it.

To make context sensitive search possible, I had to dig into the source for isearch to see what variables isearch is using. I dynamically bind to those variables before isearch does its thing. It’s a very unhealthy way to program, but it works. And that’s Emacs.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Context Sensitive iSearch
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Extend isearch so for context-sensitive search.
;;
;; If an isearch query has too many results, you can refine it by calling
;; `isearch-constraint-forward' or `isearch-constraint-backward'
;; to constrain results by the context around the initial query.
;; For example, having searched `foo', you can refine the query
;; by specifying only those foo's that are followed by a bar.

;;;; variables from isearch.el
;; `isearch-string' is the accumulator used by minibuffer to build up the search query.
;; `isearch-opoint' is where the search first started, to allow jump back.
;; `isearch-message' is used to display the search query in the minibuffer. I am not really using it properly.

(defvar isearch-constraint-anchor ""
  "This is the search string one started with. Go to that after the constraints.")
(defvar isearch-constraint-opoint 0
  "Where the search was first started.")

(defvar isearch-constraint-direction nil)
(defvar isearch-constraint-string "")
(defvar isearch-constraint-message "")

(defun isearch-constraint-forward ()
  (interactive)
  (isearch-with-constraint 'forward))

(defun isearch-constraint-backward ()
  (interactive)
  (isearch-with-constraint 'backward))

(defun isearch-with-constraint (direction)
  (interactive)
  (unless isearch-constraint-direction
    (setq isearch-constraint-anchor isearch-string)
    (setq isearch-constraint-opoint isearch-opoint))
  (setq isearch-constraint-string
	(isearch-current-constraint-string))
  (setq isearch-constraint-direction direction)
  ;;(pr "iwc" (isearch-current-constraint-string))
  (setq isearch-string "")
  (setq isearch-message "")
  (isearch-search-and-update)
  )

(defun isearch-current-constraint-string ()
  (case isearch-constraint-direction
    (forward (concat
	      (if (equal "" isearch-constraint-string) ""
		  (concat isearch-constraint-string ".*?"))
	      (if isearch-regexp isearch-string
		  (regexp-quote isearch-string))))
    (backward (concat
	       (if isearch-regexp isearch-string
		   (regexp-quote isearch-string))
	       (if (equal "" isearch-constraint-string) ""
		   (concat ".*?" isearch-constraint-string))))
    (t (if isearch-regexp isearch-string
	   (regexp-quote isearch-string)))))

;; an after advice is needed for isearch-done to clear isearch-stored-string
(defadvice isearch-done (after isearch-clear-constraint)
  (when isearch-constraint-direction
    (setq isearch-constraint-string "")
    (setq isearch-constraint-message "")
    (setq isearch-constraint-direction nil)
    ;; Go to the initial search we were looking for.
    ;;; if last isearch is forward, we'd be at the end of the match
    ;;; Or we'd be at the beginning of the match
    (if isearch-forward
	(progn (re-search-backward isearch-constraint-anchor)
	       (re-search-forward isearch-constraint-anchor))
	(re-search-forward isearch-constraint-anchor))
    (set-mark isearch-constraint-opoint)
    (setq isearch-constraint-opoint 0)
    ))

;; two identical advices for the isearch functionalities I need.
;;; you might want others, i dunno. Dig into the source yourself.
;; These two allow search and repeated search.
(defadvice isearch-repeat (around isearch-with-constraint)
  "Perform a context sensitive search using the previous input as constraint."
  ;;(pr "isr" (isearch-current-constraint-string) isearch-forward)
  (let ((isearch-string (isearch-current-constraint-string))
	(isearch-regexp t))
    ad-do-it))

(defadvice isearch-search-and-update (around isearch-with-constraint)
  ;;(pr "isau" (isearch-current-constraint-string))
  (let ((isearch-string (isearch-current-constraint-string))
	(isearch-regexp t))
    ad-do-it))

(ad-activate 'isearch-repeat)
(ad-activate 'isearch-search-and-update)
(ad-activate 'isearch-done)

(provide 'isearch-constraint)

;; (ad-update-all)