How to get visually selected text in VimScript

Vim

Vim Problem Overview


I'm able to get the cursor position with getpos(), but I want to retrieve the selected text within a line, that is '<,'>. How's this done?

UPDATE

I think I edited out the part where I explained that I want to get this text from a Vim script...

Vim Solutions


Solution 1 - Vim

I came here asking the same question as the topic starter and tried the code by Luc Hermitte but it didn't work for me (when the visual selection is still in effect while my code is executed) so I wrote the function below, which seems to work okay:

function! s:get_visual_selection()
    let [line_start, column_start] = getpos("'<")[1:2]
    let [line_end, column_end] = getpos("'>")[1:2]
    let lines = getline(line_start, line_end)
    if len(lines) == 0
        return ''
    endif
    let lines[-1] = lines[-1][: column_end - 2]
    let lines[0] = lines[0][column_start - 1:]
    return join(lines, "\n")
endfunction

I hope this is useful to someone!

Update (May 2013): Actually that's not quite correct yet, I recently fixed the following bug in one of the Vim plug-ins I published:

function! s:get_visual_selection()
    " Why is this not a built-in Vim script function?!
    let [line_start, column_start] = getpos("'<")[1:2]
    let [line_end, column_end] = getpos("'>")[1:2]
    let lines = getline(line_start, line_end)
    if len(lines) == 0
        return ''
    endif
    let lines[-1] = lines[-1][: column_end - (&selection == 'inclusive' ? 1 : 2)]
    let lines[0] = lines[0][column_start - 1:]
    return join(lines, "\n")
endfunction

Update (May 2014): This (trivial) code is hereby licensed as public domain. Do with it what you want. Credits are appreciated but not required.

Solution 2 - Vim

On Linux, there is a cheap but effective alternative to programming such a GetVisualSelection() function yourself: use the * register!

The * register contains the content of the most recent Visual selection. See :h x11-selection.

In your script you could then simply access @* to get the Visual selection.

let v = @*

Incidentally, * is also a neat little helper in interactive use. For example, in insert mode you can use CTRL-R * to insert what you had selected earlier. No explicit yanking involved.

This works only on operating systems that support the X11 selection mechanism.

Solution 3 - Vim

The best way I found was to paste the selection into a register:

function! lh#visual#selection()
  try
    let a_save = @a
    normal! gv"ay
    return @a
  finally
    let @a = a_save
  endtry
endfunction

Solution 4 - Vim

This is quite an old question, but since I can imagine lots of people will come across it at some point, here's my modified version of @xolox answer

function! VisualSelection()
	if mode()=="v"
		let [line_start, column_start] = getpos("v")[1:2]
		let [line_end, column_end] = getpos(".")[1:2]
	else
		let [line_start, column_start] = getpos("'<")[1:2]
		let [line_end, column_end] = getpos("'>")[1:2]
	end
	if (line2byte(line_start)+column_start) > (line2byte(line_end)+column_end)
		let [line_start, column_start, line_end, column_end] =
		\   [line_end, column_end, line_start, column_start]
	end
	let lines = getline(line_start, line_end)
	if len(lines) == 0
			return ''
	endif
	let lines[-1] = lines[-1][: column_end - 1]
	let lines[0] = lines[0][column_start - 1:]
	return join(lines, "\n")
endfunction
  1. '< and '> don't get updated when the user is still in visual mode, thus . and v need to be used in that case.
  2. It is possible to select text backwards in visual mode, which means '> comes before '< in the text. In those cases the two positions simply need to be reversed.
  3. While this isn't in my version of the function, one could choose to reverse the string if the selection was backwards. Here's a snipped that shows how to do this.

Assuming the variable "reverse" is defined when the marks are in reverse order:

if exists("reverse")
	let lines_r = []
	for line in lines
		call insert(lines_r, join(reverse(split(line, ".\\zs"))))
	endfor
	return join(lines_r, "\n")
else
	return join(lines, "\n")
end

Solution 5 - Vim

I'm not totally sure about the context here, because getpos() can indeed accept marks (like '< and '>) as arguments.

However, to take a stab at what you might be asking for, there's also v, which is like '< except it's always updated (i.e. while the user is still in visual mode). This can be used in combination with ., the current cursor position, which will then represent the end of the visual selection.

Edit: I found these in :help line(); several functions including line() and getpos() have the same set of possible arguments.

Edit: I guess you're probably simply asking how to get the text between two arbitrary marks, not going line-by-line... (i.e. this doesn't specifically pertain to visual mode). I don't think there actually is a way. Yes, this seems like a pretty glaring omission. You should be able to fake it by finding the marks with getpos(), getting all the lines with getline(), then chopping off on the first and last according to the column position (with casework depending on whether or not it's multi-line). Sorry it's not a real answer, but at least you can wrap it up in a function and forget about it.

Solution 6 - Vim

I once wrote a function that is able to do it without touching registers or cursor position:

function s:F.map.getvrange(start, end)
    let [sline, scol]=a:start
    let [eline, ecol]=a:end
    let text=[]
    let ellcol=col([eline, '$'])
    let slinestr=getline(sline)
    if sline==eline
        if ecol>=ellcol
            call extend(text, [slinestr[(scol-1):], ""])
        else
            call add(text, slinestr[(scol-1):(ecol-1)])
        endif
    else
        call add(text, slinestr[(scol-1):])
        let elinestr=getline(eline)
        if (eline-sline)>1
            call extend(text, getline(sline+1, eline-1))
        endif
        if ecol<ellcol
            call add(text, elinestr[:(ecol-1)])
        else
            call extend(text, [elinestr, ""])
        endif
    endif
    return text
endfunction

It is called like this:

let [sline, scol, soff]=getpos("'<")[1:]
let [eline, ecol, eoff]=getpos("'>")[1:]
if sline>eline || (sline==eline && scol>ecol)
    let [sline, scol, eline, ecol]=[eline, ecol, sline, scol]
endif
let lchar=len(matchstr(getline(eline), '\%'.ecol.'c.'))
if lchar>1
    let ecol+=lchar-1
endif
let text=s:F.map.getvrange([sline, scol], [eline, ecol])

Note that at this point you will have a list of strings in text: one reason why I wrote this function is ability to keep NULLs in file. If you stick with any solution that yanks text in a register all NULLs will be replaced with newlines and all newlines will be represented as newlines as well. In the output of getvrange function though NULLs are represented as newlines while newlines are represented by different items: there is an NL between each list item, just like in output of getline(start, end).

This function can only be used to get lines for characterwise selection (as for linewise it is much simpler and for blockwise I iterate over lines and do not need such function. There are also functions for deleting given range (without touching registers) and inserting text at given position (without touching registers or cursor).

Solution 7 - Vim

Added block selection to @xolox's great answer: mode() isnt used cause it this is planned to be used in function whereby the selection has been cleared by calling the operator etc.

xnoremap <leader>a :<C-U> call GetVisualSelection(visualmode())<Cr>

function! GetVisualSelection(mode)
    " call with visualmode() as the argument
    let [line_start, column_start] = getpos("'<")[1:2]
    let [line_end, column_end]     = getpos("'>")[1:2]
    let lines = getline(line_start, line_end)
    if a:mode ==# 'v'
        " Must trim the end before the start, the beginning will shift left.
        let lines[-1] = lines[-1][: column_end - (&selection == 'inclusive' ? 1 : 2)]
        let lines[0] = lines[0][column_start - 1:]
    elseif  a:mode ==# 'V'
        " Line mode no need to trim start or end
    elseif  a:mode == "\<c-v>"
        " Block mode, trim every line
        let new_lines = []
        let i = 0
        for line in lines
            let lines[i] = line[column_start - 1: column_end - (&selection == 'inclusive' ? 1 : 2)]
            let i = i + 1
        endfor
    else
        return ''
    endif
    for line in lines
        echom line
    endfor
    return join(lines, "\n")
endfunction

Solution 8 - Vim

This function taken from vim-asterisk also works in <expr> mappings, supports block-wise selections and multibyte columns.

function! GetVisualSelection()
    let mode = mode()
    let end_col = s:curswant() is s:INT.MAX ? s:INT.MAX : s:get_col_in_visual('.')
    let current_pos = [line('.'), end_col]
    let other_end_pos = [line('v'), s:get_col_in_visual('v')]
    let [begin, end] = s:sort_pos([current_pos, other_end_pos])
    if s:is_exclusive() && begin[1] !=# end[1]
        " Decrement column number for :set selection=exclusive
        let end[1] -= 1
    endif
    if mode !=# 'V' && begin ==# end
        let lines = [s:get_pos_char(begin)]
    elseif mode ==# "\<C-v>"
        let [min_c, max_c] = s:sort_num([begin[1], end[1]])
        let lines = map(range(begin[0], end[0]), '
        \   getline(v:val)[min_c - 1 : max_c - 1]
        \ ')
    elseif mode ==# 'V'
        let lines = getline(begin[0], end[0])
    else
        if begin[0] ==# end[0]
            let lines = [getline(begin[0])[begin[1]-1 : end[1]-1]]
        else
            let lines = [getline(begin[0])[begin[1]-1 :]]
            \         + (end[0] - begin[0] < 2 ? [] : getline(begin[0]+1, end[0]-1))
            \         + [getline(end[0])[: end[1]-1]]
        endif
    endif
    return join(lines, "\n") . (mode ==# 'V' ? "\n" : '')
endfunction

let s:INT = { 'MAX': 2147483647 }

" @return Number: return multibyte aware column number in Visual mode to select
function! s:get_col_in_visual(pos) abort
    let [pos, other] = [a:pos, a:pos is# '.' ? 'v' : '.']
    let c = col(pos)
    let d = s:compare_pos(s:getcoord(pos), s:getcoord(other)) > 0
    \   ? len(s:get_pos_char([line(pos), c - (s:is_exclusive() ? 1 : 0)])) - 1
    \   : 0
    return c + d
endfunction

function! s:get_multi_col(pos) abort
    let c = col(a:pos)
    return c + len(s:get_pos_char([line(a:pos), c])) - 1
endfunction

" Helper:

function! s:is_visual(mode) abort
    return a:mode =~# "[vV\<C-v>]"
endfunction

" @return Boolean
function! s:is_exclusive() abort
    return &selection is# 'exclusive'
endfunction

function! s:curswant() abort
    return winsaveview().curswant
endfunction

" @return coordinate: [Number, Number]
function! s:getcoord(expr) abort
    return getpos(a:expr)[1:2]
endfunction

"" Return character at given position with multibyte handling
" @arg [Number, Number] as coordinate or expression for position :h line()
" @return String
function! s:get_pos_char(...) abort
    let pos = get(a:, 1, '.')
    let [line, col] = type(pos) is# type('') ? s:getcoord(pos) : pos
    return matchstr(getline(line), '.', col - 1)
endfunction

" @return int index of cursor in cword
function! s:get_pos_in_cword(cword, ...) abort
    return (s:is_visual(get(a:, 1, mode(1))) || s:get_pos_char() !~# '\k') ? 0
    \   : s:count_char(searchpos(a:cword, 'bcn')[1], s:get_multi_col('.'))
endfunction

" multibyte aware
function! s:count_char(from, to) abort
    let chars = getline('.')[a:from-1:a:to-1]
    return len(split(chars, '\zs')) - 1
endfunction

" 7.4.341
" http://ftp.vim.org/vim/patches/7.4/7.4.341
if v:version > 704 || v:version == 704 && has('patch341')
    function! s:sort_num(xs) abort
        return sort(a:xs, 'n')
    endfunction
else
    function! s:_sort_num_func(x, y) abort
        return a:x - a:y
    endfunction
    function! s:sort_num(xs) abort
        return sort(a:xs, 's:_sort_num_func')
    endfunction
endif

function! s:sort_pos(pos_list) abort
    " pos_list: [ [x1, y1], [x2, y2] ]
    return sort(a:pos_list, 's:compare_pos')
endfunction

function! s:compare_pos(x, y) abort
    return max([-1, min([1,(a:x[0] == a:y[0]) ? a:x[1] - a:y[1] : a:x[0] - a:y[0]])])
endfunction

Solution 9 - Vim

I think you should use "clipboard resgisiter".

For more detail, you might read help ':h clipboard-autoselect'

If you enable this option( set clipboard=unnamed,autoselected),
you ca get selected text more easily like this " let l:text = @* "

Solution 10 - Vim

Modified version of @DarkWiiPlayer answer.

The differences are:

1. It test &selection to give the proper functionality when

:behave mswin

As well as the default:

:behave xterm

2. It also works properly for visual block selections testing visualmode()

I'm also returned the visual selection as an array of lines (becuase that is what I needed). However I've left the return join(lines, "\n") in commented out if you need a signal text block instead.

function! VisualSelection()
    if mode()=="v"
        let [line_start, column_start] = getpos("v")[1:2]
        let [line_end, column_end] = getpos(".")[1:2]
    else
        let [line_start, column_start] = getpos("'<")[1:2]
        let [line_end, column_end] = getpos("'>")[1:2]
    end

    if (line2byte(line_start)+column_start) > (line2byte(line_end)+column_end)
        let [line_start, column_start, line_end, column_end] =
        \   [line_end, column_end, line_start, column_start]
    end
    let lines = getline(line_start, line_end)
    if len(lines) == 0
            return ['']
    endif
	if &selection ==# "exclusive"
	    let column_end -= 1 "Needed to remove the last character to make it match the visual selction
	endif
    if visualmode() ==# "\<C-V>"
        for idx in range(len(lines))
            let lines[idx] = lines[idx][: column_end - 1]
            let lines[idx] = lines[idx][column_start - 1:]
        endfor
    else
        let lines[-1] = lines[-1][: column_end - 1]
        let lines[ 0] = lines[ 0][column_start - 1:]
    endif
    return lines  "use this return if you want an array of text lines
    "return join(lines, "\n") "use this return instead if you need a text block
endfunction

Solution 11 - Vim

Overview:

visually select, press mapping, reselect original selection with gv and copy it to a register, finally paste from the register

Use case:

  1. Add function Test() to your vimrc:

> function! Test() range
> exe 'sp temp.tmp'
> exe 'norm p'
> endfunction

  1. Open a new file
  2. Create the mapping ,m
    :vmap ,m :norm gvy<Esc>:call Test()<CR>
  3. visually select some text
  4. press ,m (the selection is gone, but 'norm gv' reselects it and 'y' yanks it to current register)
  5. Test() is called: file temp.tmp is opened and 'norm p' pastes from current register, which is the original visual selection

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionguillermoooView Question on Stackoverflow
Solution 1 - VimxoloxView Answer on Stackoverflow
Solution 2 - VimgltsView Answer on Stackoverflow
Solution 3 - VimLuc HermitteView Answer on Stackoverflow
Solution 4 - VimDarkWiiPlayerView Answer on Stackoverflow
Solution 5 - VimCascabelView Answer on Stackoverflow
Solution 6 - VimZyXView Answer on Stackoverflow
Solution 7 - Vimrun_the_raceView Answer on Stackoverflow
Solution 8 - VimEnnoView Answer on Stackoverflow
Solution 9 - VimtkngueView Answer on Stackoverflow
Solution 10 - Vimpev.hallView Answer on Stackoverflow
Solution 11 - Vimuser1501439View Answer on Stackoverflow