0

I am building custom text object in lua. With the following code, I am able to change content within certain symbols. This is my code:

local in_symbol = function(start_symbol, end_symbol, mode)
  local v = vim.api
  local currentLineNum = v.nvim_win_get_cursor(0)[1]
  local end_match = vim.api.nvim_call_function("searchpos", {start_symbol, "", currentLineNum})
  local start_match = vim.api.nvim_call_function("searchpos", {start_symbol, "b", currentLineNum})
  if start_match[1] == 0 then
    return
  end
  if end_match[1] == 0 then
    end_match = vim.api.nvim_call_function("searchpos", {start_symbol, "n", currentLineNum})
  end
  if end_match[1] == 0 then
    return
  end
  local end_symbol_length = string.len(end_symbol)
  local end_offset = mode == 'i' and end_match[2] - end_symbol_length - 1 or end_match[2] - 1

  local start_symbol_length = string.len(start_symbol)
  local start_offset = mode == 'i' and start_match[2] or start_match[2]
  if end_match[2] - start_match[2] == 1 then
    v.nvim_win_set_cursor(0, { currentLineNum, start_offset - 1})
    print('no content')
    return
  end
  v.nvim_win_set_cursor(0, { currentLineNum, start_offset})
  vim.cmd('normal! v')
  v.nvim_win_set_cursor(0, { currentLineNum, end_offset})
end

return {
  in_symbol = in_symbol,
}

For a custom text object for *(i.e. ci*), I have the following mapping

vim.cmd [[xnoremap <silent> i* :lua require('plugins.custom_text_objects').in_symbol('*' ,'*', 'i')<cr>]] 
vim.cmd [[onoremap <silent> i* :lua require('plugins.custom_text_objects').in_symbol('*' ,'*', 'i')<cr>]] 

This works well with a sentence like this:

*hello*

But when there is no content (i.e. **), it will just eat up the first or the second * on ci*. How can I fix this behavior? I cannot think of a good solution right now.

4
  • Nothing to do with your question but it looks like you're assigning to start_offset the result of boolean expression mode == 'i' and start_match[2] or start_match[2]. That's a redundant way of saying start_match[2]. ;)
    – B Layer
    May 7 at 14:48
  • You might like the "targets.vim" plug-in: github.com/wellle/targets.vim, it implements many smarts around text objects such as i* etc. It will find the most appropriate one (if you're in it, sitting on the last character, find next one in line if you're not inside one) and in many cases properly handle multiple pairs of occurrences in the same line. (If you have this *word* and that *word* it will not select the ` and that ` part if you use an i* there, since *word* is the appropriate match.)
    – filbranden
    May 7 at 15:00
  • 1
    @BLayer Thanks. I getting crazy with this and totally overlooked this mistake. May 7 at 15:50
  • No problem. It's not a critical error; logic isn't changed. It could come back to bite you later, though, so I figured I'd give you a heads up. Cheers.
    – B Layer
    May 7 at 18:46
0

An onoremap will not act on anything if you exit the function without moving the cursor, so you should try to do that.

Your initial calls to searchpos() don't include the n flag, so they're doing that.

Here's a very simple and naive proof of concept (in pure Vimscript) to show how this works:

function! FindStar()
  let [_, s] = searchpos('*', 'bcn', line('.'))
  if s <= 0
    return
  endif
  let [_, e] = searchpos('*', 'n', line('.'))
  if e <= 0 || e <= s+1
    return
  endif
  call cursor('.', s+1)
  execute 'normal! v'
  call cursor('.', e-1)
endfunction

onoremap <silent> i* :<C-u>call FindStar()<CR>

Note how both calls to searchpos() are passing the n flag, so they're not moving the cursor. The initial one also passes c to possibly match at the current position.

They both check for the result being zero (or negative, which shouldn't happen) to cover for the case of no match being found in the current line.

But then the second case also checks for e <= s+1, which is exactly the case where the stars are next to each other and the content is empty in that case. Simply going ahead and returning in that case, without having moved the cursor, will ensure nothing will have been selected.

The code then goes ahead to move the cursor and prepare a visual selection.

Take that for a spin (or perhaps convert this to Lua if you like), you'll see it covers that corner case nicely.

6
  • 1
    Thank you for your answer. But it seems that it doens't work when the cursor is one the last symbol initially like this *hello[*]. It will not pick up the symbol correctly. May 7 at 16:27
  • Also for **, if my cursor is on the [*]*, it will jump to []** instead of *[]*. May 7 at 16:31
  • Yes, correct, it will not handle cursor over the last character and there are other corner cases that will break. Like I mentioned, what I wrote is a "very simple and naive" proof of concept. My point was just to demonstrate how to handle an empty selection of **, which is what your question is asking... To handle all corner cases, you need code that's way more complex than this. For that, I recommend picking up a plug-in such as "targets.vim" which implements this all in a very smart way!
    – filbranden
    May 7 at 17:06
  • 1
    of course, targets.vim just defers answering the stated question as it uses quite crazy tricks to implement this itself
    – Mass
    May 7 at 20:03
  • The question is “How to handle no content for a custom text object?” The answer is “by returning without moving the cursor” and my snippet illustrates that clearly. If the question was “How to reliably implement i*?”, then my answer would have been “Don't! Just use targets.vim or a similar existing plug-in.”
    – filbranden
    May 7 at 20:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.