vim-surround icon indicating copy to clipboard operation
vim-surround copied to clipboard

Auto detect surrounding character to be replaced

Open fgarcia opened this issue 10 years ago • 15 comments

Would it be possible to replace the surrounding character without typing the original character? Like

"Hello world"
     ^

Right now if my cursor is inside the string I must type :cs"{ but sometimes I just feel very lazy and would rather let the script finding out the surrounding character and just type :cs{

fgarcia avatar Jan 30 '15 12:01 fgarcia

What if I have foo{"hello world"} and I want to replace the braces with parens?

In my opinion, automatically detecting the user's intention is not very Vim-like. Instead, let the user be precise, and the editor will behave predictably.

tommcdo avatar Jan 30 '15 13:01 tommcdo

Good point, I am totally against modifying the existing behavior for the :cs command.

I should have requested for a new command name for it. And although the automatic detection is enough for most of my use cases, I would be glad to have one that just replaces the surrounding character under the cursor.

Somehow in most cases I look at my code and spot a surrounding character I want to change, I have unconsciously already moved my cursor over the offender. Therefore it feels redundant typing in the character under my cursor.

Automatic surrounding block detection would be just a nice to have. Replacing the under cursor character is quite precise and complaining about one extra keystroke certainly is very Vim-like :innocent:

fgarcia avatar Jan 30 '15 15:01 fgarcia

Leaving the existing cs operator intact and creating a new one for auto-detection is not a bad idea. Maybe cd (change detected character) would be a good choice?

I'm still not totally sold on the idea, but it's not my decision. I'm just some guy!

tommcdo avatar Jan 30 '15 16:01 tommcdo

I think that it is very beneficial to have this feature. under the cursor and automatic detection are both good ideas I think. When automatic detections wouldn't work is pretty obvious, so everyone can manually place cursor so that cd would change the surrounding character that they wanted. And change under cursor is also very useful.

You see every time I'm using this command to change {} to [] I'm spending precious time on thinking whether I need to hold shift or not. I think everyone would agree that the combination of cs SHIFT bracket bracket without shift is not very easy to type. Add the problem of choosing between left and right bracket and you will see, that in 99% cases I'm (and I believe most people) spending a lot more energy and time than I really need to.

@fgarcia Did you managed to implementing change under cursor?

purpleP avatar Feb 24 '16 15:02 purpleP

I never found a good solution, but I wrote a small function to perform my most common substitutions, so I can quickly replace natural opposites.

function! MySurround()
    let word_under_cursor = expand("<cword>")
    let current_char = getline(".")[col(".")-1]
    if current_char == "'"
        execute "normal cs'\""
    elseif current_char == '"'
        execute "normal cs\"'"
    elseif current_char == '{'
        execute "normal ,b"
    elseif word_under_cursor == 'do'
        execute "normal ,b"
    elseif word_under_cursor == 'end'
        execute "normal ,b"
    else
        echo "Unknown opposite"
    end
endfunction
nmap <leader>s :call MySurround()<CR>

fgarcia avatar Feb 24 '16 17:02 fgarcia

Hmm... It seems to me that maybe creating another plugin inspired by this one would be beneficial to many (myself included). One thing though is I don't know vimscript at all, but I guess it's no harder than python or Haskell or a couple other languages I know. My idea is that given a map wrapper = {'{': '}', '}': '{'} etc that could be user extensible we should implement functions

  1. Detect wrapping (surrounding). Probably all of them from the most innermost to most outermost (there shouldn't be a lot of them in any real scenario).
  2. Quickly jump from wrapper start to it's opposite (which would be wrapping end). Vim is able to do that already AFAIK (but not with custom wrappers)
  3. Change innermost wrapper to another. A special case of that would be when cursor is already on the innermost wrapper. I can made a few other but this is the core functions. If anyone would be kind enough to guide me on how to do that I would certainly implement it. For example my questions is - how vim currently find wrappers when I'm pressing the '%'? Do we need to copy this functionality, or we can use it somehow? Maybe custom surroundings should be actually implemented in vim (in neovim for example) and the functionality to change them in plugin? Or all of this functionality should be in plugin?

purpleP avatar Feb 25 '16 07:02 purpleP

Another advantage of this suggestion is that it could fix the problem described in #30, which I just encountered. cs'" doesn't work for me, but ds" does, so being able to do just cs" (or cd") would be awesome.

dbmrq avatar Jul 07 '16 21:07 dbmrq

@purpleP I don't know much about vimscript either, there's probably a better solution, but one way to do this would be to get the character under the cursor (which I know vimscript can do) and see if it matches any characters used for surroundings. Then, if it doesn't, look at the next character to the left, and so on, until we have the surrounding. When we find the right character, it's just a matter of returning cs<character><input>.

dbmrq avatar Jul 07 '16 21:07 dbmrq

Chiming in to address a couple of (somewhat old) points:

When automatic detections wouldn't work is pretty obvious, so everyone can manually place cursor so that cd would change the surrounding character that they wanted.

How often can you move your cursor in fewer than 1 keystroke? Cause that's all it takes to specify the character you want to replace. Usually, the fastest way to move your cursor to a particular character is with the f command (and family), which requires typing that character anyway.

Add the problem of choosing between left and right bracket and you will see, that in 99% cases I'm (and I believe most people) spending a lot more energy and time than I really need to.

On keyboards that right-align the printed characters on the keys, there's a visual mnemonic built in: the left brackets are right up against the edge of the key no with space to their right (which would be inside the brackets), and the right brackets do have space to their left. That's consistent with whether cs and ys will insert a space.

tommcdo avatar Jul 07 '16 21:07 tommcdo

@tommcdo Those are good arguments for adding this as a new command, but I still think it would be a huge advantage to have it, especially considering what I mentioned about issue #30. Right now I have to do something like ds'ysiw" to get the effect of cs'". So being able to do just cd' would be amazing.

dbmrq avatar Jul 07 '16 21:07 dbmrq

Ok, here's my very rough attempt at doing something like this:

function! ChangeDetectedSurrounding()
    let chars = ['(', '[', '{', '<', '"', '`', "'",
               \ '.', ',', ';', ':', '~', '!', '?', '/', '\', '|']
    let column = 1
    while col('.')-column >= 0
        let char = getline('.')[col('.')-column]
        if index(chars, char) >= 0
            return "cs" . char
        endif
        let column += 1
    endwhile
endfunction

nmap <expr> cd ChangeDetectedSurrounding()

Now when the cursor is inside a surrounding I can just do cd" and the surrounding will become a double quote. 🎉

I expect this to have many problems, and it'll only work for the characters inside that chars array, so no html tags or anything like that. But it solves my problem with issue #30, so I'm happy. Hopefully someone will come up with a better option, but for now this gets the job done.

dbmrq avatar Jul 07 '16 22:07 dbmrq

There are some really tricky situations to consider here. Consider the following:

bar(foo(some, thing), other);

If your cursor is on the word thing, what would you expect to be detected as surrounding? The above would choose ,, but given the context I think you'd definitely want (.

How could we account for this?

There's a common issue with most features that automatically detect what you want to do. Ultimately, it usually means the feature has to understand the syntax and grammar of the programming language you're using in order to do anything sensible. Without that, the feature just becomes an annoyance that you have to use very defensively.

tommcdo avatar Jul 09 '16 14:07 tommcdo

Yes, I agree something like this would have some unavoidable problems. But I still think those cases are exceptions, and then I can place the cursor right above what I want to change or just use the regular cs (which I agree is faster in most cases, but I can't use it for quotes).

That being said, your comment did give me an idea of how to improve the function, so here's version 2.0:

function! LookLeft(char)
    let column = 2
    while col('.')-column >= 0
        let char = getline('.')[col('.')-column]
        if char == a:char
            return 1
        endif
        let column += 1
    endwhile
    return 0
endfunc

function! LookRight(char)
    let column = 0
    while col('.')+column <= col('$')
        let char = getline('.')[col('.')+column]
        if char == a:char
            return 1
        endif
        let column += 1
    endwhile
    return 0
endfunc

function! ChangeDetectedSurrounding()
    let pairs = {'(': ')', '[': ']', '{': '}', '<': '>',
               \ '`': '`', '"': '"', "'": "'"}
    let chars = ['.', ',', ';', ':', '~', '-', '=',
               \ '!', '?', '/', '\', '|']
    let surroundings = copy(chars)
    for pair in items(pairs)
        call extend(surroundings, pair)
    endfor
    let char = getline('.')[col('.')-1]
    if index(surroundings, char) >= 0
        echo "cs" . char
        return "cs" . char
    endif
    for pair in items(pairs)
        if LookLeft(pair[0]) && LookRight(pair[1])
            echo "cs" . pair[0]
            return "cs" . pair[0]
        endif
    endfor
    for char in chars
        if LookLeft(char) && LookRight(char)
            echo "cs" . char
            return "cs" . char
        endif
    endfor
endfunction

nmap <expr> cd ChangeDetectedSurrounding()

Now I first check if the cursor is above any surrounding characters, and if it is I call cs with that character. If it isn't, I first check if the cursor is inside any pairs of parentheses, brackets or quotes, and if it is, I go with that. At last, I check other random characters, like commas and dots. It's still not perfect, but pretty good I think.

I don't know much about vim script (it must be obvious from my code), so any improvements are welcome. That for loop to extend the sorroundings list looks pretty bad, for instance, but I didn't know how to do it better. Also I don't know if these functions should have different scopes or something? I don't even know how that works, I just read that it exists right now. And I don't even know if this should be in my .vimrc, by the way. Well, I know next to nothing, haha. So any help is much appreciated.

dbmrq avatar Jul 09 '16 20:07 dbmrq

I haven't looked into it, but you might be able to draw some ideas from expand-region on how to automatically detect the nearest pair of things.

tommcdo avatar Jul 09 '16 22:07 tommcdo

In https://github.com/machakann/vim-sandwich , this is "srb(" - change closest surround to (

"sr" - change surround (which one?) "b" - closest one (to what?) "(" - to that

There's also sdb etc.

sethidden avatar Jun 08 '21 23:06 sethidden