views:

318

answers:

3

I have written this script that replaces many spaces around the cursor with one space. This however doesn't work when I use it with no spaces around the cursor. It seems to me that Vim doesn't replace on a zero-width match.

function JustOneSpace()
    let save_cursor = getpos(".")
    let pos = searchpos(' \+', 'bc')
    s/\s*\%#\s*/ /e
    let save_cursor[2] = pos[1] + 1 
    call setpos('.', save_cursor)
endfunction

nmap <space> :call JustOneSpace()<cr>

Here are a few examples (pipe | is cursor):

This line

hello     |      world

becomes

hello |world

But this line

hello wo|rld

doesn't become

hello wo |rld

Update: By changing the function to the following it works for the examples above.

function JustOneSpace()
    let save_cursor = getpos(".")
    let pos = searchpos(' *', 'bc')
    s/\s*\%#\s*/ /e
    let save_cursor[2] = pos[1] + 1 
    call setpos('.', save_cursor)
endfunction

This line

hello |world

becomes

hello w|orld

The problem is that the cursors moves to the next character. It should stay in the same place.

Any pointers and or tips?

+3  A: 

I don't use vim, but if you want to match zero or more spaces, shouldn't you be using ' *' instead of ' \+' ?

EDIT: re the cursor positioning problem: what you're doing now is setting the position at the beginning of the whitespace before you do the substitution, then moving it forward one position so it's after the space. Try setting it at the end of the match instead, like this:

search(' *', 'bce')

That way, any additions or removals will occur before the cursor position. In most editors, the cursor position automatically moves to track such changes. You shouldn't need to do any of that getpos/setpos stuff.

Alan Moore
Alan is correct, you want \*. Using that modification causes the script to behave as you describe you want it to.
Adam Bellaire
I just tried this, but there is still one problem. I will update the question with the example.
Peter Stuifzand
+3  A: 

I think that the only problem with your script is that the position saving doesn't seem correct. You can essentially do what you are trying to do with:

:s/\s*\%#\s*/ /e

which is identical to the (correct) code in your question. You could simply map this with:

:nmap <space> :s/\s*\%#\s*/ /e<CR>

If you want to save the position, it gets a little more complicated. Probably the best bet is to use something like this:

function! JustOneSpace()
    " Get the current contents of the current line
    let current_line = getline(".")
    " Get the current cursor position
    let cursor_position = getpos(".")
    " Generate a match using the column number of the current cursor position
    let matchRE = '\(\s*\)\%' . cursor_position[2] . 'c\s*'
    " Find the number of spaces that precede the cursor
    let isolate_preceding_spacesRE = '^.\{-}' . matchRE . '.*$'
    let preceding_spaces = substitute(current_line, isolate_preceding_spacesRE, '\1', "")
    " Modify the line by replacing with one space
    let modified_line = substitute(current_line, matchRE, " ", "")
    " Modify the cursor position to handle the change in string length
    let cursor_position[2] -= len(preceding_spaces) - 1
    " Set the line in the window
    call setline(".", modified_line)
    " Reset the cursor position
    call setpos(".", cursor_position)
endfunction

Most of that is comments, but the key thing is that you look at the length of the line before and after the substitution and decide on the new cursor position accordingly. You could do this with your method by comparing len(getline(".")) before and after if you prefer.

Edit

If you want the cursor to end after the space character, modify the line:

    let cursor_position[2] -= len(current_line) - len(modified_line)

such that it looks like this:

    let cursor_position[2] -= (len(current_line) - len(modified_line)) - 1

Edit (2)

I've changed the script above to consider your comments such that the cursor position is only adjusted by the number of spaces before the cursor position. This is done by creating a second regular expression that extracts the spaces preceding the cursor (and nothing else) from the line and then adjusting the cursor position by the number of spaces.

Al
This function works almost. There is one problem: it will move the cursor to the front of the space character instead of behind it.
Peter Stuifzand
Ah, I didn't know exactly where you wanted the cursor to end, so I just picked one at random. See the edit above.
Al
After a bit of extra research it seems your function doesn't work at all. There are a lot of cursor positions possible between two words if there are a lot of spaces. The cursor should be moved by the number of spaces in front of the cursor to get to the right position, not the total number of spaces removed. If you try this with the cursor at other positions between the words you can see this for yourself. But I think I have found a way to get the right results.
Peter Stuifzand
Good point. I've modified the function above to fix this.
Al
Yes, it seems your function works now as it should.
Peter Stuifzand
A: 

This function is based on Al's answer.

function JustOneSpace()
    " Get the current contents of the current line
    let current_line = getline(".")

    " Get the current cursor position
    let cursor_position = getpos(".")

    " Generate a match using the column number of the current cursor position
    let matchre = '\s*\%' . cursor_position[2] . 'c\s*'
    let pos = match(current_line, matchre) + 2

    " Modify the line by replacing with one space
    let modified_line = substitute(current_line, matchre, " ", "")

    " Modify the cursor position to handle the change in string length
    let cursor_position[2] = pos

    " Set the line in the window
    call setline(".", modified_line)
    " Reset the cursor position
    call setpos(".", cursor_position)
endfunction

Instead using the difference between the normal and the modified line, I find the position of the first space that will match the regular expression of the substitution. Then I set the cursor position to that position + 1.

Peter Stuifzand
I've modified my function to do this an alternative way (by calculating the number of spaces before the cursor prior to changing the line).
Al