Open zbirenbaum opened 1 year ago
I think what you're looking for is opts.highlight_unlabeled_phase_one_targets = true
. It is opt-in, since it's actually redundant, and Leap aims for reducing the visual noise as much as possible by default. I also added an entry to the FAQ on how to set "Lightspeed-style" highlighting.
I think what you're looking for is
opts.highlight_unlabeled_phase_one_targets = true
. It is opt-in, since it's actually redundant, and Leap aims for reducing the visual noise as much as possible by default. I also added an entry to the FAQ on how to set "Lightspeed-style" highlighting.
Thank you for the response. I was using that option, but I don't think it does quite what I was looking for. I pasted in the entry from the faq on lightspeed style highlighting, but the only time matches are highlighted is if they are unique and don't have a red label next to them. What I was looking for was for all of them to be highlighted, regardless of whether it has a label or not.
As an update, I managed to reproduce the documentation highlighting, it just didn't work how I thought it did.
What I really wanted was the ability to clear the backdrop highlight from chars pressed, and highlight the next possible next chars in the sequence with a different color than the label or the highlight applied to the unlabeled matches.
Without the backdrop, it is too difficult to see immediately what the jump options are, but with the backdrop it is too difficult to read the next character in the sequence. I realize this isn't an issue if you know the two chars ahead of time, but I would prefer to have progressive feedback as sometimes I lose my train of thought.
I tried implementing this with a action function, but it doesn't execute until after I have selected two chars and a label so it is impossible.
I have followed your development of lightspeed and leap for a while, and I recognize this might not be consistent with your goals for the project, as some would argue it encourages a more 'step-by-step' approach. I believe that this what leap was written to avoid, so feel free to close this if the feature is something you would rather not have as part of the project.
Nevermind, I managed to implement it. Here's a demo and the source.
The code is kinda messy since I hacked it together, but for anyone who wants something similar, here it is:
local keymap = vim.keymap
local leap = require('leap')
local ns = vim.api.nvim_create_namespace('leap_custom')
local prio = 65535
leap.setup({
highlight_unlabeled_phase_one_targets = false,
})
local api = vim.api
local hex_to_rgb = function(hex_str)
local hex = "[abcdef0-9][abcdef0-9]"
local pat = "^#(" .. hex .. ")(" .. hex .. ")(" .. hex .. ")$"
hex_str = string.lower(hex_str)
assert(string.find(hex_str, pat) ~= nil, "hex_to_rgb: invalid hex_str: " .. tostring(hex_str))
local red, green, blue = string.match(hex_str, pat)
return { tonumber(red, 16), tonumber(green, 16), tonumber(blue, 16) }
end
local blend = function(fg, bg, alpha)
fg = hex_to_rgb(fg)
bg = hex_to_rgb(bg)
local blendChannel = function(i)
local ret = (alpha * fg[i] + ((1 - alpha) * bg[i]))
return math.floor(math.min(math.max(0, ret), 255) + 0.5)
end
return string.format("#%02X%02X%02X", blendChannel(1), blendChannel(2), blendChannel(3))
end
local hl_char_one = '#FFFFFF'
local hl_char_two = blend(hl_char_one, '#565c64', .5)
vim.api.nvim_set_hl(0, 'LeapHighlightChar1', {fg = '#FFFFFF', bold = true})
vim.api.nvim_set_hl(0, 'LeapHighlightChar2', {fg = hl_char_two, bold = true})
local extmarks = {}
local state = { prev_input = nil }
local function custom_motion(kwargs)
require('leap').opts.safe_labels = {}
local function get_input()
vim.cmd('echo ""')
local hl = require('leap.highlight')
if vim.v.count == 0 and not (kwargs.unlabeled and vim.fn.mode(1):match('o')) then
hl['apply-backdrop'](hl, kwargs.cc.backward)
end
hl['highlight-cursor'](hl)
vim.cmd('redraw')
local ch = require('leap.util')['get-input-by-keymap']({str = ">"})
hl['cleanup'](hl, { vim.fn.getwininfo(vim.fn.win_getid())[1] })
if not ch then
return
end
-- Repeat with the previous input?
local repeat_key = require('leap.opts').special_keys.repeat_search
if ch == api.nvim_replace_termcodes(repeat_key, true, true, true) then
if state.prev_input then
ch = state.prev_input
else
require('leap.util').echo('no previous search')
return
end
else
state.prev_input = ch
end
return ch
end
local function get_pattern(input, max)
local chars = require('leap.opts').eq_class_of[input]
if chars then
chars = vim.tbl_map(function (ch)
if ch == '\n' then
return '\\n'
elseif ch == '\\' then
return '\\\\'
else return ch end
end, chars or {})
input = '\\(' .. table.concat(chars, '\\|') .. '\\)' -- "\(a\|b\|c\)"
end
return '\\V' .. (kwargs.multiline == false and '\\%.l' or '') .. input
end
local function get_targets(pattern, max)
local search = require('leap.search')
local bounds = search['get-horizontal-bounds']()
local get_char_at = require('leap.util')['get-char-at']
local targets = {}
for pos in search['get-match-positions'](
pattern, bounds, { ['backward?'] = kwargs.cc.backward }
) do
local char1 = get_char_at(pos, {})
local char2 = get_char_at({pos[1], pos[2]+1}, {})
table.insert(targets, { pos = {pos[1], pos[2]+1 }, chars={ char1, char2 } })
end
return targets
end
-- local get_targets = require('leap.search')['get-targets']
local input = get_input()
local pattern = get_pattern(input)
local targets = get_targets(pattern, {})
for _, target in ipairs(targets) do
local extmark_pos = tostring(target.pos[1]) .. ':' .. tostring(target.pos[2])
extmarks[extmark_pos] = {
vim.api.nvim_buf_set_extmark(0, ns, target.pos[1]-1, target.pos[2]-2, {
hl_group = 'LeapHighlightChar1',
end_col = target.pos[2]-1,
strict = false,
priority = prio,
}),
vim.api.nvim_buf_set_extmark(0, ns, target.pos[1]-1, target.pos[2]-1, {
hl_group = 'LeapHighlightChar2',
end_col = target.pos[2],
strict = false,
priority = prio,
})
}
end
local input2 = get_input()
if input2 then
input = input .. input2
else
return {}
end
pattern = get_pattern(input)
local new_targets = get_targets(pattern, {})
for i, target in ipairs(new_targets) do
extmarks[tostring(target.pos[1]) .. ':' .. tostring(target.pos[2]-1)] = nil
new_targets[i].pos = { target.pos[1], target.pos[2]-1 }
end
for _, id in pairs(extmarks) do
vim.api.nvim_buf_del_extmark(0, ns, id[1])
vim.api.nvim_buf_del_extmark(0, ns, id[2])
end
return new_targets
end
local create_mappings = function ()
local modes = { 'n', 'x', 'o' }
local opts = { noremap = true, silent = true }
local mappings = {
['s'] = { backward = false },
['S'] = { backward = true },
}
for mapping, opt in pairs(mappings) do
keymap.set(modes, mapping, function ()
require('leap').leap({
targets = custom_motion({ cc = opt }),
offset = 0
})
end, opts)
end
end
create_mappings()
vim.api.nvim_set_hl(0, 'LeapBackdrop', { link = 'Comment' })
vim.api.nvim_set_hl(0, 'LeapMatch', {
fg = 'white',
bold = true,
nocombine = true,
})
vim.api.nvim_create_autocmd('User', {
pattern = 'LeapLeave',
callback = function ()
vim.api.nvim_buf_clear_namespace(0, ns, 0, -1)
end
})
There's a small bug when the character is at the end of the line with nothing after it, but it shouldn't be too hard to fix. I'd like to have the label show after the first keypress as leap usually does, but my instinct is that that's not going to be easy to implement. I'll take a look at fixing the end of line issue and label issue some time later, as it is quite late now.
I recognize this might not be consistent with your goals for the project, as some would argue it encourages a more 'step-by-step' approach. I believe that this what leap was written to avoid
Yes, exactly. The very idea is that you simply look at the target, type the two characters you see, and then type the label, if appears, it's not some step-by-step "okay... I've entered the first one, let's see, what shall I do next..."-kind of process. The label is the only relevant information, everything else (except in some rare cases) is redundant noise.
Nevermind, I managed to implement it. Here's a demo and the source.
Wow, this is impressive though :)
I've been thinking about exposing a beacon
callback, and optional argument to leap
(similar to action
), that gets the target as argument. (The target structure has the position, the label, and the matched characters themselves, so everything that is needed to create a custom "beacon".) One problem is that it must be "phase-independent" (otherwise it would be too complicated), that is, you couldn't implement things like making the match highlight disappear after the second input, like in your demo.
I've been thinking about exposing a beacon callback, and optional argument to leap (similar to action), that gets the target as argument. (The target structure has the position, the label, and the matched characters themselves, so everything that is needed to create a custom "beacon".) One problem is that it must be "phase-independent" (otherwise it would be too complicated), that is, you couldn't implement things like making the match highlight disappear after the second input, like in your demo.
I'd love to see this! Don't worry about it being phase agnostic. Currently, light-up-beacons
is private, but if users can make a callback within it which would allow for setting up user extmarks or override its behavior completely (which is what it sounds like you are thinking about doing), then everything is super easy.
Clearing the highlights can be done by overriding the cleanup
function in leap.highlight
which executes after each keypress, and therefore is capable of deleting user extmarks at any point. Since it also receives self as a parameter, most of the relevant information known to leap is accessible.
If the user doesn't care about the efficiency of looping over the extmarks an additional time, this is as simple as:
local cleanup = require('leap.highlight').cleanup
require('leap.highlight').cleanup = function (self, affected_windows)
print('User can make callback here')
cleanup(self, affected_windows)
end
If the user doesn't mind their code being a bit longer, for very minor efficiency gains, they could do this:
local util = require("leap.util")
local inc = util["inc"]
local dec = util["dec"]
require('leap.highlight').cleanup = function(self, affected_windows)
for _, context in ipairs(self.extmarks) do
local bufnr = context[1]
local id = context[2]
-- Here is the inserted callback
print('user can make callback for specific extmark without an additional loop over self.extmarks here')
api.nvim_buf_del_extmark(bufnr, self.ns, id)
end
self.extmarks = {}
if pcall(api.nvim_get_hl_by_name, self.group.backdrop, false) then
for _, wininfo in ipairs(affected_windows) do
api.nvim_buf_clear_namespace(wininfo.bufnr, self.ns, dec(wininfo.topline), wininfo.botline)
end
return api.nvim_buf_clear_namespace(0, self.ns, dec(vim.fn.line("w0")), vim.fn.line("w$"))
else
return nil
end
end
All the user has to do is maintain a user_extmarks
table indexed by the id of the leap extmarks, and they can find the id of the additional extmarks they placed by doing user_extmarks[id]
and then delete it with nvim_buf_del_extmark.
Clearing the highlights can be done by overriding the cleanup function in leap.highlight which executes after each keypress, and therefore is capable of deleting user extmarks at any point.
I think it's even simpler, users can add their extmarks to the highlight.extmarks
table, just like the native light-up-beacons
does, and then they will be cleaned up together with the default ones in each phase, won't they?
Currently, light-up-beacons is private, but if users can make a callback within it which would allow for setting up user extmarks or override its behavior completely
That sounds pretty clean actually... the callback might return a boolean, so one might override the whole thing, or just decorate the original function if they want:
(light-up-beacons [targets ...]
(when (or (not user-callback) (user-callback targets ...))
(<default stuff>)))
Currently, light-up-beacons is private, but if users can make a callback within it which would allow for setting up user extmarks or override its behavior completely
That sounds pretty clean actually... the callback might return a boolean, so one might override the whole thing, or just decorate the original function if they want:
(light-up-beacons [targets ...] (when (or (not user-callback) (user-callback targets ...)) (<default stuff>)))
I've been running into issues lately since I currently have to override a lot of the behavior in ways that that break frequently when code logic or function signatures change.
Do you have an idea of an implementation spec for this so I can get the ball rolling on a PR?
I just added that line as it is, as an undocumented "escape hatch", so that you folks can experiment with it right away. Feedback is welcome. The advantage of using an opts
field is that you can make general tweaks to the highlighting, in addition to call-specific ones.
Example - this is how one could implement highlight_unlabeled_phase_one_targets
for themselves, without setting it in opts
:
function highlight_unlabeled_phase_one_targets(targets, first_idx, last_idx)
local hl = require('leap.highlight')
for i = first_idx or 1, last_idx or #targets do
local target = targets[i]
if not target.label and target.chars then
local bufnr = target.wininfo.bufnr
local id = vim.api.nvim_buf_set_extmark(
bufnr, hl.ns, target.pos[1] - 1, target.pos[2] - 1,
{
virt_text = { { table.concat(target.chars), "LeapMatch" } },
virt_text_pos = "overlay",
hl_mode = "combine",
priority = hl.priority.label,
}
)
-- This way Leap automatically cleans up your stuff together with its own.
table.insert(hl.extmarks, { bufnr, id })
end
end
-- Continue with Leap's native function body.
return true
end
require('leap').opts.on_beacons = highlight_unlabeled_phase_one_targets
To always highlight all matches, just remove not target.label
from the the condition:
if target.chars then
I have read through the readme and the help file, and tried every highlight group present, but I can't replicate the photo in the readme which shows a search where typed characters in a search have their own highlight group. Was this feature removed for some reason?
For eye strain and focus reasons, I'd like to have a grey backdrop, where if I press
s
followed bya
, all 'a' characters will be highlighted according to some highlight group. Ideally, a separate highlight group would also be available for all characters following the first one, so that I can see more clearly what the different options are.Right now leap feels a bit 'dead' compared to lightspeed, and the lack of visual feedback is making it really hard to swap over.