Fricitionless depth-first search with :grep and friends

When I’m walking the results of :grep, :lsp-references and friends, I often want to do another recursive search.
Running :grep again destroys the old search buffer and with it the context of my original search, which is annoying.
We can rename the *grep* buffer but that’s a manual step that I might forget to do. Also, it becomes tedious to pick names after multiple levels of recursion.

To make this easier I have each :grep command push its resulting buffer onto a stack.
When I’m done with the current buffer, I can pop from the stack and return to the state before the latest search.

This allows to do a depth-first search across a code base, without having to keep any context in my head.

Here is the code with some example mappings.
I want to package it (as plugin or in the stdlib) but it currently generates a lot of garbage buffers, I’d like to solve this first. Maybe we can have hidden buffers…

# example mappings to traverse search results:
# temporary dependency on kak-lsp for brevity
map global normal <c-n> %{:lsp-next-location %opt{locations_stack_top}<ret>}
map global normal <c-p> %{:lsp-previous-location %opt{locations_stack_top}<ret>}
map global normal <c-r> %{:locations-pop<ret>}

declare-option -hidden str-list locations_stack
declare-option -hidden str locations_stack_top

hook global WinDisplay \*(?:callees|callers|diagnostics|goto|find|grep|implementations|lint-output|references|symbols)\*(?:-\d+)? %{
	locations-push
}

define-command locations-push -docstring "push a new locations buffer onto the stack" %{
	evaluate-commands %sh{
		eval set -- $kak_quoted_opt_locations_stack
		if printf '%s\n' "$@" | grep -Fxq -- "$kak_bufname"; then
		exit # already in the stack
		fi
		# rename to avoid conflict with *grep* etc.
		newname=$kak_bufname-$#
		echo "try %{ delete-buffer! $newname }"
		echo "rename-buffer $newname"
	}
	set-option -add global locations_stack %val{bufname}
	set-option global locations_stack_top %sh{
		eval set -- $kak_quoted_opt_locations_stack
		eval echo \"\$$#\"
	}
}

define-command locations-pop -docstring "pop a locations buffer from the stack and return to previous location" %{
	evaluate-commands %sh{
		eval set -- $kak_quoted_opt_locations_stack
		if [ $# -lt 2 ]; then
		echo "fail locations-pop: no locations buffer to pop"
		fi
	}
	delete-buffer %opt{locations_stack_top}
	set-option -remove global locations_stack %opt{locations_stack_top}
	set-option global locations_stack_top %sh{
		eval set -- $kak_quoted_opt_locations_stack
		eval echo \"\$$#\"
	}
	try %{
		evaluate-commands -try-client %opt{jumpclient} %{
			buffer %opt{locations_stack_top}
			grep-jump
		}
	}
}

define-command locations-clear -docstring "delete locations buffers" %{
	evaluate-commands %sh{
		eval set --  $kak_quoted_opt_locations_stack
		printf 'try %%{ delete-buffer %s }\n' "$@"
	}
	set-option global locations_stack
}
3 Likes

Thats just lovely.

Awesome! This is something I’ve wanted for a while.

The locations-pop command is only partially working for me. It does remove the latest search buffer, but doesn’t jump back to the previous location. I tracked down grep-jump, and will paste it here for reference. This issue could well be something with my specific set up, but nothing immediately jumps out at me. I’ll have to come back to it later.

define-command -hidden grep-jump %{
    evaluate-commands %{ # use evaluate-commands to ensure jumps are collapsed
        try %{
            execute-keys '<a-x>s^((?:\w:)?[^:]+):(\d+):(\d+)?<ret>'
            set-option buffer grep_current_line %val{cursor_line}
            evaluate-commands -try-client %opt{jumpclient} -verbatim -- edit -existing %reg{1} %reg{2} %reg{3}
            try %{ focus %opt{jumpclient} }
        }
    }
}

Also, I have an inkling that the right solution to the “garbage” buffers problem could be really useful for a lot more than this specific case.

sorry I really should have tested it (I use a slightly different implementation). Fixed locations-pop, thanks. My mistake was that I somehow assumed hooks would fire before the command is done.

I have an idea for reducing the number of buffers: we can store the grep results in an option instead.

While trying to determine whether the behavior I was seeing was intentional or bugged, I started using this command to inspect the options at different points during usage:

define-command -override locations-debug \
-docstring 'prints the values of locations options to the debug buffer' %{
    echo -debug "DEBUG LOCATIONS"
    echo -debug "locations_stack_top:"
    echo -debug %opt{locations_stack_top}
    echo -debug "locations_stack:"
    echo -debug %opt{locations_stack}
}

This revealed that location buffers were getting duplicated in the list when I returned to them. I assume this was not intended, and wound up reworking the locations-push command to prevent it:

define-command -override locations-push \
-docstring "push a new locations buffer onto the stack" %{
    evaluate-commands %sh{
        eval set -- $kak_quoted_opt_locations_stack
        if printf '%s\n' "$@" | grep -Fxq -- "$kak_bufname"; then
            # already in the stack
            printf "%s\n" "echo -debug 'locations buffer already in the stack'"
        else
            # rename to avoid conflict with *grep* etc.
            newname=$kak_bufname-$#
            echo "try %{ delete-buffer! $newname }"
            echo "rename-buffer $newname"
            echo "set-option -add global locations_stack %val{bufname}"
        fi
    }
}

I’m still not sure whether it was intended, but the original locations-push would always push the buffer name because the exit in the shell block only cut short the execution in that block. Note that I’m also still relying the original hook for updating locations_stack_top here.

There was another issue I saw, which I’m guessing is actually grep.kak and not this code. When <c-n>'ing through grep results, ga will usually take me back to the grep/location buffer regardless of how many results I skipped through. This is nice, but would sometimes “break”, meaning that ga would simply revert to “go to previous buffer” as it normally does. There’s something fancy going on there that I don’t understand, but to ensure I can always get back to the current search buffer I added this simple but helpful command, and mapped it to <c-g>:

define-command -override locations-goto-current \
-docstring "go to the current locations buffer" %{
    try %{
        buffer %opt{locations_stack_top}
    } catch %{
        echo -debug "locations_stack_top not found"
    }
}

Be warned that I didn’t check whether these snippets are compatible with the updated code–I’m running a slightly modified local copy.

With these changes, I have this working pretty well for traversing down and back up a stack of grep-like search results. I have lots of other thoughts related to this (buffer groups, save/restore search session…) but will leave it at that for now.

1 Like

right, your fix is correct, I wrongly moved the set -add outside the scope of the early exit. Sadly I can’t edit the topic anymore :frowning:

I had the same experience, that was pretty nice.
This is probably because of the way ga uses the jump list.
I’m not sure if it still works, I also defined a shortcut for that buffer.

Posting a fixed and ready-to-use version because I need to link to it.
I guess it could be a plugin but I think it’s such a generally useful thing that I’d try to put it in the stdlib.

evaluate-commands %sh{kak-lsp -s ${kak_session} --kakoune}

map global normal <c-n> %{:lsp-next-location %opt{grep_buffer}<ret>} -docstring 'lsp next'
map global normal <c-p> %{:lsp-previous-location %opt{grep_buffer}<ret>} -docstring 'lsp previous'
map global normal <c-r> %{:grep-stack-pop<ret>} -docstring 'grep-stack-pop'

# grep-like buffers from Kakoune, LSP and kakoune-find.
hook -group grep-stack global WinDisplay \*(?:callees|callers|diagnostics|goto|find|grep|implementations|lint-output|references)\*(?:-\d+)? %{
	grep-stack-push
}

declare-option -hidden str grep_buffer
declare-option -hidden str-list grep_stack
define-command -override grep-stack-push -docstring "record grep buffer" %{
	evaluate-commands %sh{
		eval set -- $kak_quoted_opt_grep_stack
		if printf '%s\n' "$@" | grep -Fxq -- "$kak_bufname"; then {
			exit
		} fi
		newbuf=$kak_bufname-$#
		echo "try %{ delete-buffer! $newbuf }"
		echo "rename-buffer $newbuf"
		echo "set-option -add global grep_stack %val{bufname}"
	}
	set-option global grep_buffer %val{bufname}
}
define-command -override grep-stack-pop -docstring "restore grep buffer" %{
	evaluate-commands %sh{
		eval set -- $kak_quoted_opt_grep_stack
		if [ $# -lt 2 ]; then {
			fail "grep-stack-pop: no grep buffer to pop"
		} fi
		printf 'set-option global grep_stack'
		while [ $# -ge 2 ]; do {
			top=$1
			printf ' %s' "$1"
			shift
		} done
		echo
		echo "delete-buffer $1"
		echo "set-option global grep_buffer '$top'"
	}
	try %{
		evaluate-commands -try-client %opt{jumpclient} %{
			buffer %opt{grep_buffer}
			grep-jump
		}
		evaluate-commands -client %opt{toolsclient} %{
			buffer %arg{1}
		}
	}
}
define-command -override grep-stack-clear -docstring "clear grep buffers" %{
	evaluate-commands %sh{
		eval set --  $kak_quoted_opt_grep_stack
		printf 'try %%{ delete-buffer %s }\n' "$@"
	}
	set-option global grep_stack
}
1 Like

When running :grep, how about trying to copy an existing *grep* buffer, running grep with the fifo and pasting the copied save to the top of the buffer?

It could be nice for Kakoune to be able to use the -fifo flag with an existing buffer, so it just appends.

I like the idea of rendering all searches in the same buffer. I think for everday use it needs an extra binding to manually clear existing searches.
I wouldn’t append on top but add a new “child node” of search results at opt{grep_current_line}
because it’s important to me that the editor records where I left off (sometimes I need to visit every single result).

# grep Vim README.asciidoc
README.asciidoc:31:28:regularly beating the best Vim solution.
README.asciidoc:41:1:Vim editor (after which Kakoune was originally inspired).
  # grep doc/ README.asciidoc
  README.asciidoc:575:7:See <<doc/pages/keys#goto-commands,`:doc keys goto-commands`>>.
  README.asciidoc:583:7:See <<doc/pages/keys#view-commands,`:doc keys view-commands`>>.
  README.asciidoc:590:7:See <<doc/pages/keys#marks,`:doc keys marks`>>.
    # grep autoload README.asciidoc
    README.asciidoc:335:65:First, Kakoune will search recursively for `.kak` files in the `autoload`
    README.asciidoc:336:39:directory. It will first look for an `autoload` directory at
    README.asciidoc:337:14:`${userconf}/autoload` and will fallback to `${runtime}/autoload` if
README.asciidoc:48:55:and incremental results, while being competitive with Vim in terms of keystroke count.

I just do :db to clear the results

The indent would break the grep format

For the grep current line option, it could be removed by using the location in the tools client

lsp-next-location already works fine here and we can also add make grep-next-match ignore leading spaces if that becomes a thing

For the grep current line option, it could be removed by using the location in the tools client

Possible; I think I got used to the current behavior though

I’ve started a WIP PR Improve ergonomics of multiple grep/git buffers by krobelus · Pull Request #5105 · mawww/kakoune · GitHub which is similar solution that uses Kakoune’s native buflist. Though the single-buffer approach might be better in some cases, I should try that.