How to get visually selected text in VimScript

Viewed 28251

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...

11 Answers

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

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

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
Related