Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vim: Invert string (by words)

This is my string:

"this is my sentence"

I would like to have this output:

"sentence my is this"

I would like to select a few words on a line (in a buffer) and reverse it word by word.

Can anyone help me?

like image 717
Reman Avatar asked Apr 03 '11 20:04

Reman


1 Answers

It's not totally clear what the context is here: you could be talking about text in a line in a buffer or about a string stored in a VimScript variable.

note: Different interpretations of the question led to various approaches and solutions.
There are some "old updates" that start about halfway through that have been rendered more or less obsolete by a plugin mentioned just above that section. I've left them in because they may provide useful info for some people.

full line replacement

So to store the text from the current line in the current buffer in a vimscript variable, you do

let words = getline('.')

And then to reverse their order, you just do

let words = join(reverse(split(words)))

If you want to replace the current line with the reversed words, you do

call setline('.', words)

You can do it in one somewhat inscrutable line with

call setline('.', join(reverse(split(getline('.')))))

or even define a command that does that with

command! ReverseLine call setline('.', join(reverse(split(getline('.')))))

partial-line (character-wise) selections

As explained down in the "old updates" section, running general commands on a character- or block-wise visual selection — the former being what the OP wants to do here — can be pretty complicated. Ex commands like :substitute will be run on entire lines even if only part of the line is selected using a character-wise visual select (as initiated with an unshifted v).

I realized after the OP commented below that reversing the words in a partial-line character-wise selection can be accomplished fairly easily with

:s/\%V.*\%V./\=join(reverse(split(submatch(0))))/

wherein

  • \%V within the RE matches some part of the visual selection. Apparently this does not extend after the last character in the selection: leaving out the final . will exclude the last selected character.
  • \= at the beginning of the replacement indicates that it is to be evaluated as a vimscript expression, with some differences.
  • submatch(0) returns the entire match. This works a bit like perl's $&, $1, etc., except that it is only available when evaluating the replacement. I think this means that it can only be used in a :substitute command or in a call to substitute()

So if you want to do a substitution on a single-line selection, this will work quite well. You can even pipe the selection through a system command using ...\=system(submatch(0)).

multiple-line character-wise selections

This seems to also work on a multiple-line character-wise selection, but you have to be careful to delete the range (the '<,'> that vim puts at the beginning of a command when coming from visual mode). You want to run the command on just the line where your visual selection starts. You'll also have to use \_.* instead of .* in order to match across newlines.

block-wise selections

For block-wise selections, I don't think there's a reasonably convenient way to manipulate them. I have written a plugin that can be used to make these sorts of edits less painful, by providing a way to run arbitrary Ex commands on any visual selection as though it were the entire buffer contents.

It is available at https://github.com/intuited/visdo. Currently there's no packaging, and it is not yet available on vim.org, but you can just git clone it and copy the contents (minus the README file) into your vimdir.

If you use vim-addon-manager, just clone visdo in your vim-addons directory and you'll subsequently be able to ActivateAddons visdo. I've put in a request to have it added to the VAM addons repository, so at some point you will be able to dispense with the cloning and just do ActivateAddons visdo.

The plugin adds a :VisDo command that is meant to be prefixed to another command (similarly to the way that :tab or :silent work). Running a command with VisDo prepended will cause that command to run on a buffer containing only the current contents of the visual selection. After the command completes, the buffer's contents are pasted into the original buffer's visual selection, and the temp buffer is deleted.

So to complete the OP's goal with VisDo, you would do (with the words to be reversed selected, and with the above-defined ReverseLine command available):

:'<,'>VisDo ReverseLine


old updates

...previous updates follow ... warning: verbose, somewhat obselete, and mostly unnecessary...

The OP's edit makes it more clear that the goal here is to be able to reverse the words contained in a visual selection, and specifically a character-wise visual selection.

This is decidedly not a simple task. The fact that vim does not make this sort of thing easy really confused me when I first started using it. I guess this is because its roots are still very much in the line-oriented editing functionality of ed and its predecessors and descendants. For example, the substitute command :'<,'>s/.../.../ will always work on entire lines even if you are in character-wise or block-wise (ctrlv) visual selection mode.

Vimscript does make it possible to find the column number of any 'mark', including the beginning of the visual selection ('<) and the end of the visual selection ('>). That is, as far as I can tell, the limit of its support. There is no direct way to get the contents of the visual selection, and there is no way to replace the visual selection with something else. Of course, you can do both of those things using normal-mode commands (y and p), but this clobbers registers and is kind of messy. But then you can save the initial registers and then restore them after the paste...

So basically you have to go to sort of extreme lengths to do with parts of lines what can easily done with entire lines. I suspect that the best way to do this is to write a command that copies the visual selection into a new buffer, runs some other command on it, and then replaces the original buffer's visual selection with the results, deleting the temp buffer. This approach should theoretically work for both character-wise and block-wise selections, as well as for the already-supported linewise selections. However, I haven't done that yet.

This 40-line code chunk declares a command ReverseCharwiseVisualWords which can be called from visual mode. It will only work if the character-wise visual selection is entirely on a single line. It works by getting the entire line containing the visual selection (using getline()) running a parameterized transformation function (ReverseWords) on the selected part of it, and pasting the whole partly-transformed line back. In retrospect, I think it's probably worth going the y/p route for anything more featureful.

" Return 1-based column numbers for the start and end of the visual selection.
function! GetVisualCols()
  return [getpos("'<")[2], getpos("'>")[2]]
endfunction

" Convert a 0-based string index to an RE atom using 1-based column index
" :help /\%c
function! ColAtom(index)
  return '\%' . string(a:index + 1) . 'c'
endfunction

" Replace the substring of a:str from a:start to a:end (inclusive)
" with a:repl
function! StrReplace(str, start, end, repl)
  let regexp = ColAtom(a:start) . '.*' . ColAtom(a:end + 1)
  return substitute(a:str, regexp, a:repl, '')
endfunction

" Replace the character-wise visual selection
" with the result of running a:Transform on it.
" Only works if the visual selection is on a single line.
function! TransformCharwiseVisual(Transform)
  let [startCol, endCol] = GetVisualCols()

  " Column numbers are 1-based; string indexes are 0-based
  let [startIndex, endIndex] = [startCol - 1, endCol - 1]

  let line = getline("'<")
  let visualSelection = line[startIndex : endIndex]
  let transformed = a:Transform(visualSelection)
  let transformed_line = StrReplace(line, startIndex, endIndex, transformed)

  call setline("'<", transformed_line)
endfunction

function! ReverseWords(words)
  return join(reverse(split(a:words)))
endfunction

" Use -range to allow range to be passed
" as by default for commands initiated from visual mode,
" then ignore it.
command! -range ReverseCharwiseVisualWords
      \ call TransformCharwiseVisual(function('ReverseWords'))

update 2

It turns out that doing things with y and p is a lot simpler, so I thought I'd post that too. Caveat: I didn't test this all too thoroughly, so there may be edge cases.

This function replaces TransformCharwiseVisual (and some of its dependencies) in the previous code block. It should theoretically work for block-wise selections too — it's up to the passed Transform function to do appropriate things with line delimiters.

function! TransformYankPasteVisual(Transform)
  let original_unnamed = [getreg('"'), getregtype('"')]
  try
    " Reactivate and yank the current visual selection.
    normal gvy
    let @" = a:Transform(@")
    normal gvp
  finally
    call call(function('setreg'), ['"'] + original_unnamed)
  endtry
endfunction

So then you can just add a second command declaration

command! -range ReverseVisualWords
      \ call TransformYankPasteVisual(function('ReverseWords'))

tangentially related gory detail

Note that the utility of a higher-level function like the ones used here is somewhat limited by the fact that there is no (easy or established) way to declare an inline function or block of code in vimscript. This wouldn't be such a limitation if the language weren't meant to be used interactively. You could write a function which substitutes its string argument into a dictionary function declaration and returns the function. However, dictionary functions cannot be called using the normal invocation syntax and have to be passed to call call(dictfunct, args, {}).

note: A more recent update, given above, obsoletes the above code. See the various sections preceding old updates for a cleaner way to do this.

like image 70
intuited Avatar answered Oct 21 '22 07:10

intuited