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
}
2 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.