Skip to content

Instantly share code, notes, and snippets.

@DanSM-5
Last active February 28, 2025 02:08
Show Gist options
  • Save DanSM-5/9283605c65af19b9a72086e7fc1027e2 to your computer and use it in GitHub Desktop.
Save DanSM-5/9283605c65af19b9a72086e7fc1027e2 to your computer and use it in GitHub Desktop.
Function to get the text from the visual selection in the vim editor. Vimscript version is compatible in both vim and neovim. Also lua only version.
-- Function to get the selected text.
-- It handles when current mode is visual mode and when visual mode has ended.
-- It handle visual, visual line and visual block modes
-- The return is a list with the selected text per line from top to bottom and from left to right.
--
-- Based on the answers in the stackoverflow post:
-- https://stackoverflow.com/questions/1533565/how-to-get-visually-selected-text-in-vimscript/28398359
--
-- It has great answers but none quite behave as I expected, so this is my take on it.
-- If you want a lua version, check this up.
-- This is a best effor translation from the vimscript version.
-- If you find an issue or have a suggestion to improve it, feel free to
-- make the suggestion.
---Get the text between the marks a and b using the appropriate mode
---@param a_mark string Reference mark a
---@param b_mark string Reference mark b
---@param mode string Mode to process text from marks
---@return string[] Selected text. One entry per line from left to right.
local get_selected_text_marks = function (a_mark, b_mark, mode)
local _, line_start, column_start = unpack(vim.fn.getpos(a_mark))
local _, line_end, column_end = unpack(vim.fn.getpos(b_mark))
-- Mark could be reversed if starting selection from bottom to top or right to left
if (vim.fn.line2byte(line_start)+column_start) > (vim.fn.line2byte(line_end)+column_end) then
line_start, column_start, line_end, column_end = line_end, column_end, line_start, column_start
end
-- Should always be an array when passing two arguments
local lines = vim.fn.getline(line_start, line_end) --[[@as string[] ]]
-- No selection, return empty
if #lines == 0 then
return {}
end
-- Handle visual line selection
if mode == 'V' then
return lines -- No further process
end
-- Handle visual block selection
if mode == vim.keycode('<C-V>') then
-- Selection can be reversed if started from right to left
if column_start > column_end then
column_start, column_end = column_end, column_start
end
if vim.o.selection == 'exclusive' then
column_end = column_end - 1 -- Needed to remove the last character to make it match the visual selction
end
for idx = 1, #lines do
-- Get just the selected area from each line
lines[idx] = lines[idx]:sub(1, column_end)
lines[idx] = lines[idx]:sub(column_start)
end
return lines
end
-- Handle visual mode 'v'
if vim.o.selection == 'exclusive' then
column_end = column_end - 1 -- Needed to remove the last character to make it match the visual selction
end
-- Adjust first and last selected lines to the respective start/end position
lines[#lines] = lines[#lines]:sub(1, column_end)
lines[1] = lines[1]:sub(column_start)
return lines
end
---Get text from visual selected area
---@return string[] Selected text. One entry per line from left to right.
local get_selected_text = function ()
local mode = vim.fn.mode()
if mode == 'v' or mode == 'V' or mode == vim.keycode('<C-V>') then
return get_selected_text_marks('v', '.', mode)
else
return get_selected_text_marks("'<", "'>", vim.fn.visualmode())
end
end
" Function to get the selected text.
" It handles when current mode is visual mode and when visual mode has ended.
" It handle visual, visual line and visual block modes
" The return is a list with the selected text per line from top to bottom and from left to right.
"
" Based on the answers in the stackoverflow post:
" https://stackoverflow.com/questions/1533565/how-to-get-visually-selected-text-in-vimscript/28398359
"
" It has great answers but none quite behave as I expected, so this is my take on it.
" Get selected text from mark positions
function! get_text_from_marks(a_mark, b_mark, mode)
let mode = a:mode
let [line_start, column_start] = getpos(a:a_mark)[1:2]
let [line_end, column_end] = getpos(a:b_mark)[1:2]
" Mark could be reversed if starting selection from bottom to top or right to left
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)
" No selection, return empty
if len(lines) == 0
return []
endif
" Handle visual line selection
if mode ==# 'V'
return lines " No further process
endif
" Handle visual block selection
if mode ==# "\<C-V>"
" Selection can be reversed if started from right to left
let [column_start, column_end] = column_end > column_start ? [column_start, column_end] : [column_end, column_start]
if &selection ==# "exclusive"
let column_end -= 1 " Needed to remove the last character to make it match the visual selction
endif
for idx in range(len(lines))
" Get just the selected area from each line
let lines[idx] = lines[idx][: column_end - 1]
let lines[idx] = lines[idx][column_start - 1:]
endfor
return lines
endif
" Handle visual mode 'v'
if &selection ==# "exclusive"
let column_end -= 1 " Needed to remove the last character to make it match the visual selction
endif
" Adjust first and last selected lines to the respective start/end position
let lines[-1] = lines[-1][: column_end - 1]
let lines[ 0] = lines[ 0][column_start - 1:]
return lines
endfunction
function! get_selected_text() abort
" Check current mode
let curr_mode = mode()
if curr_mode ==# 'v' || curr_mode ==# 'V' || curr_mode ==# "\<C-V>"
" This is valid if currently in a type of visual mode trigger by
" something like `<cmd>call func(get_selected_text())<cr>` while in visual mode
" We can use positions 'v' (cursor) and '.' (oposite end)
return get_text_from_marks('v', '.', curr_mode)
else
" When current mode is not visual, it means the mode may have ended like from keymaps
" like `:<C-U>call func(get_selected_text())<cr>` which take you out of visual mode
" Then the last visual mode can be fetched with visualmode() and positions with the marks '< '>
return get_text_from_marks("'<", "'>", visualmode())
end
endfunction
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment