Files
Ingo Karkat 700aec39d2 ENH: Add ingo#text#{InsertNewLine{,Above,Below},ReplaceLine}Here()
System-info: Ubuntu 24.04.3 LTS, x86_64
Platform-info: Vim 9.1.2108
2026-02-04 14:28:18 +01:00

447 lines
17 KiB
VimL

" ingo/text.vim: Function for getting and setting text in the current buffer.
"
" DEPENDENCIES:
" - ingo/mbyte/virtcol.vim autoload script
" - ingo/pos.vim autoload script
" - ingo/regexp/virtcols.vim autoload script
"
" Copyright: (C) 2012-2026 Ingo Karkat
" The VIM LICENSE applies to this script; see ':help copyright'.
"
" Maintainer: Ingo Karkat <ingo@karkat.de>
function! ingo#text#Get( startPos, endPos, ... )
"*******************************************************************************
"* PURPOSE:
" Extract the text between a:startPos and a:endPos from the current buffer.
" Multiple lines will be delimited by a newline character.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:startPos [line, col]; col is the 1-based byte-index.
" a:endPos [line, col]; col is the 1-based byte-index.
" a:isExclusive Flag whether a:endPos is exclusive; by default, the
" character at that position is included; pass 1 to exclude
" it.
"* RETURN VALUES:
" string text
"*******************************************************************************
let [l:exclusiveOffset, l:exclusiveMatch] = (a:0 && a:1 ? [1, ''] : [0, '.'])
let [l:line, l:column] = a:startPos
let [l:endLine, l:endColumn] = a:endPos
if ingo#pos#IsAfter([l:line, l:column], [l:endLine, l:endColumn + l:exclusiveOffset])
return ''
endif
let l:text = ''
while 1
if l:line == l:endLine
let l:text .= matchstr(getline(l:line) . "\n", '\%' . l:column . 'c' . '.*\%' . l:endColumn . 'c' . l:exclusiveMatch)
break
else
let l:text .= matchstr(getline(l:line) . "\n", '\%' . l:column . 'c' . '.*')
let l:line += 1
let l:column = 1
endif
endwhile
return l:text
endfunction
function! ingo#text#GetFromArea( area, ... )
"*******************************************************************************
"* PURPOSE:
" Extract the text in the area of [startPos, endPos] from the current
" buffer. Multiple lines will be delimited by a newline character.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:area [[startLnum, startCol], [endLnum, endCol]]; col is the
" 1-based byte-index.
" a:isExclusive Flag whether a:endPos is exclusive; by default, the
" character at that position is included; pass 1 to exclude
" it.
"* RETURN VALUES:
" string text
"*******************************************************************************
if a:area[0][0] == 0 || a:area[1][0] == 0
return ''
endif
return call('ingo#text#Get', a:area + a:000)
endfunction
function! ingo#text#GetChar( startPos, ... )
"*******************************************************************************
"* PURPOSE:
" Extract one / a:count character(s) from a:startPos from the current buffer.
" Only considers the current line.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:startPos [line, col]; col is the 1-based byte-index.
" a:count Optional number of characters to extract; default 1.
" If this is a negative number, tries to extract as many as
" possible instead of not matching.
"* RETURN VALUES:
" string text, or empty string if no(t enough) character(s).
"*******************************************************************************
let [l:line, l:column] = a:startPos
let [l:count, l:isUpTo] = (a:0 ? (a:1 > 0 ? [a:1, 0] : [-1 * a:1, 1]) : [0, 0])
return matchstr(getline(l:line), '\%' . l:column . 'c' . '.' . (l:count ? '\{' . (l:isUpTo ? ',' : '') . l:count . '}' : ''))
endfunction
function! ingo#text#GetCharBefore( startPos, ... )
"*******************************************************************************
"* PURPOSE:
" Extract one / a:count character(s) before a:startPos from the current buffer.
" Only considers the current line.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:startPos [line, col]; col is the 1-based byte-index.
" a:count Optional number of characters to extract; default 1.
" If this is a negative number, tries to extract as many as
" possible instead of not matching.
"* RETURN VALUES:
" string text, or empty string if no(t enough) character(s).
"*******************************************************************************
let [l:line, l:column] = a:startPos
let [l:count, l:isUpTo] = (a:0 ? (a:1 > 0 ? [a:1, 0] : [-1 * a:1, 1]) : [0, 0])
return matchstr(getline(l:line), '.' . (l:count ? '\{' . (l:isUpTo ? ',' : '') . l:count . '}' : '') . '\%' . l:column . 'c')
endfunction
function! ingo#text#GetCharVirtCol( startPos, ... )
"*******************************************************************************
"* PURPOSE:
" Extract one / a:count character(s) from a:startPos from the current buffer.
" Only considers the current line.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:startPos [line, virtcol]; virtcol is the 1-based screen column.
" a:count Optional number of characters to extract; default 1.
" If this is a negative number, tries to extract as many as
" possible instead of not matching.
"* RETURN VALUES:
" string text, or empty string if no(t enough) character(s).
"*******************************************************************************
let l:startBytePos = [a:startPos[0], ingo#mbyte#virtcol#GetColOfVirtCol(a:startPos[0], a:startPos[1])]
return ingo#text#GetChar(l:startBytePos, (a:0 ? a:1 : 1))
endfunction
function! ingo#text#Insert( pos, text )
"******************************************************************************
"* PURPOSE:
" Insert a:text at a:pos.
"* ASSUMPTIONS / PRECONDITIONS:
" Buffer is modifiable.
"* EFFECTS / POSTCONDITIONS:
" Changes the buffer.
"* INPUTS:
" a:pos [line, col]; col is the 1-based byte-index.
" a:text String to insert.
"* RETURN VALUES:
" Flag whether the position existed (inserting in column 1 of one line beyond
" the last one is also okay) and insertion was done.
"******************************************************************************
let [l:lnum, l:col] = a:pos
if l:lnum > line('$') + 1
return 0
endif
let l:line = getline(l:lnum)
if l:col > len(l:line) + 1
return 0
elseif l:col < 1
throw 'Insert: Column must be at least 1'
elseif l:col == 1
return (setline(l:lnum, a:text . l:line) == 0)
elseif l:col == len(l:line) + 1
return (setline(l:lnum, l:line . a:text) == 0)
endif
return (setline(l:lnum, strpart(l:line, 0, l:col - 1) . a:text . strpart(l:line, l:col - 1)) == 0)
endfunction
function! ingo#text#Append( pos, text )
"******************************************************************************
"* PURPOSE:
" Append a:text behind a:pos.
"* ASSUMPTIONS / PRECONDITIONS:
" Buffer is modifiable.
"* EFFECTS / POSTCONDITIONS:
" Changes the buffer.
"* INPUTS:
" a:pos [line, col]; col is the 1-based byte-index. A column of 0 can be
" used to append _before_ the first character in the line (i.e. the
" same as inserting at column 1).
" a:text String to insert.
"* RETURN VALUES:
" Start byte index of the appended character; -1 (instead of 0) if the line
" was empty; the return value can also be interpreted as a flag whether the
" position existed (appending to column 1 of one line beyond the last one is
" also okay) and insertion was done.
"******************************************************************************
let [l:lnum, l:col] = a:pos
if l:lnum > line('$') + 1
return 0
endif
let l:line = getline(l:lnum)
if l:col > len(l:line)
return 0
elseif l:col < 0
throw 'Append: Column must be at least 0'
elseif l:col == 0
return (setline(l:lnum, a:text . l:line) == 0 ? -1 : 0)
elseif l:col == len(l:line)
let l:lineLen = len(l:line)
return (setline(l:lnum, l:line . a:text) == 0 ? l:lineLen : 0)
endif
let l:currentCharLength = len(matchstr(l:line, '\%' . l:col . 'c' . '.'))
return (setline(l:lnum, strpart(l:line, 0, l:col + l:currentCharLength - 1) . a:text . strpart(l:line, l:col + l:currentCharLength - 1)) == 0 ? l:col + l:currentCharLength - 1 : 0)
endfunction
function! ingo#text#IsInsert( strategy ) abort
"******************************************************************************
"* PURPOSE:
" Determine whether to insert or append at the cursor position based on the
" passed a:strategy configuration.
"* ASSUMPTIONS / PRECONDITIONS:
" None
"* EFFECTS / POSTCONDITIONS:
" None
"* INPUTS:
" a:strategy Configuration value; one of:
" - insert1: at the beginning of the line if the cursor is in column 1, else
" appending after the character the cursor is on.
" - append$: appending after the character if the cursor is at the end of the
" line (with 'virtualedit' having "onemore": one beyond the end,
" with "all": always insert before the cursor), else inserting
" before the character the cursor is on.
" - insert: always inserting before the character the cursor is on
" - append: always appending after the character the cursor is on
"* RETURN VALUES:
" 1 if insert, 0 if append.
"******************************************************************************
if a:strategy ==# 'insert1'
return (col('.') == 1)
elseif a:strategy ==# 'append$'
if ingo#option#Contains(&virtualedit, 'all')
return 1
else
return ! (ingo#option#Contains(&virtualedit, 'onemore') ? ingo#cursor#IsBeyondEndOfLine() : ingo#cursor#IsAtEndOfLine())
endif
elseif a:strategy ==# 'insert'
return 1
elseif a:strategy ==# 'append'
return 0
else
throw 'ASSERT: Invalid a:strategy: ' . a:strategy
endif
endfunction
if ! exists('g:IngoLibrary_InsertHereStrategy')
let g:IngoLibrary_InsertHereStrategy = 'insert1'
endif
function! ingo#text#InsertHere( text ) abort
"******************************************************************************
"* PURPOSE:
" Insert a:text at the cursor position; where exactly is determined by
" g:IngoLibrary_InsertHereStrategy; cp. ingo#text#IsInsert().
"* ASSUMPTIONS / PRECONDITIONS:
" Buffer is modifiable.
"* EFFECTS / POSTCONDITIONS:
" Changes the buffer.
" Sets register ".
"* INPUTS:
" a:text String to insert.
"* RETURN VALUES:
" None.
"******************************************************************************
let l:insertCommand = (ingo#text#IsInsert(g:IngoLibrary_InsertHereStrategy) ? 'i' : 'a')
execute 'normal!' l:insertCommand . a:text . "\<C-\>\<C-n>"
endfunction
function! ingo#text#InsertNewLineHere( mode, line, ... )
"******************************************************************************
"* PURPOSE:
" Insert a new line below the current line containing a:line.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" Changes the current buffer and cursor position.
"* INPUTS:
" a:mode Determines where the new line is inserted:
" 'o': below the current line
" 'O': above the current line
" 'cc': replacing the current line, store former contents in default
" register
" '"_cc": replacing the current line without storing former contents
" a:line Text for the new line; this is inserted as typed (so it can contain
" cursor movements like "\<Up>"). No comment leader will be added.
" a:options.additionalNormalModeCommands
" Additional commands to be executed after the insertion, in normal
" mode.
" a:options.isDisableAutoindent
" Flag whether the automatic indenting in the buffer should be
" disabled for the line insertion. By default, auto-indenting takes
" place as configured.
"* RETURN VALUES:
" 1 on success; 0 on failure.
"******************************************************************************
let l:options = (a:0 ? a:1 : {})
let l:additionalNormalModeCommands = get(l:options, 'additionalNormalModeCommands', '')
let l:isDisableAutoindent = get(l:options, 'isDisableAutoindent', 0)
if l:isDisableAutoindent
" ":set paste" avoids automatic insert of comment leader, and also disables
" automatic indenting.
set paste
else
" Insert the comment line without automatic insert of comment leader, but at
" the current indent.
let l:save_formatoptions = &formatoptions
set formatoptions-=o
endif
try
execute 'normal! zv' . a:mode . a:line . "\<C-\>\<C-n>" . l:additionalNormalModeCommands
return 1
finally
if l:isDisableAutoindent
set nopaste
else
let &formatoptions = l:save_formatoptions
endif
endtry
return 0
endfunction
function! ingo#text#InsertNewLineAboveHere( ... )
return call('ingo#text#InsertNewLineHere', ['O'] + a:000)
endfunction
function! ingo#text#InsertNewLineBelowHere( ... )
return call('ingo#text#InsertNewLineHere', ['o'] + a:000)
endfunction
function! ingo#text#ReplaceLineHere( ... )
return call('ingo#text#InsertNewLineHere', ['"_cc'] + a:000)
endfunction
function! ingo#text#Replace( pos, len, replacement, ... )
"******************************************************************************
"* PURPOSE:
" Replace a:len bytes of text at a:pos with a:replacement.
"* ASSUMPTIONS / PRECONDITIONS:
" Buffer is modifiable.
"* EFFECTS / POSTCONDITIONS:
" Changes the buffer.
"* INPUTS:
" a:pos [line, col]; col is the 1-based byte-index.
" a:len Number of bytes to replace.
" a:replacement Replacement text.
"* RETURN VALUES:
" Flag whether the position existed and replacement was done.
"******************************************************************************
let [l:lnum, l:col] = a:pos
if l:lnum > line('$')
return 0
endif
let l:line = getline(l:lnum)
if l:col > len(l:line)
return 0
elseif l:col < 1
throw (a:0 ? a:1 : 'Replace') . ': Column must be at least 1'
endif
return (setline(l:lnum, strpart(l:line, 0, l:col - 1) . a:replacement . strpart(l:line, l:col - 1 + a:len)) == 0)
endfunction
function! ingo#text#Remove( pos, len )
"******************************************************************************
"* PURPOSE:
" Remove a:len bytes of text at a:pos.
"* ASSUMPTIONS / PRECONDITIONS:
" Buffer is modifiable.
"* EFFECTS / POSTCONDITIONS:
" Changes the buffer.
"* INPUTS:
" a:pos [line, col]; col is the 1-based byte-index.
" a:len Number of bytes to remove.
"* RETURN VALUES:
" Flag whether the position existed and removal was done.
"******************************************************************************
return ingo#text#Replace(a:pos, a:len, '', 'Remove')
endfunction
function! ingo#text#ReplaceChar( startPos, replacement, ... )
"******************************************************************************
"* PURPOSE:
" Replace one / a:count character(s) from a:startPos with a:replacement.
"* ASSUMPTIONS / PRECONDITIONS:
" Buffer is modifiable.
"* EFFECTS / POSTCONDITIONS:
" Changes the buffer.
"* INPUTS:
" a:startPos [line, col]; col is the 1-based byte-index.
" a:replacement String to be put into the buffer.
" a:count Optional number of characters to replace; default 1.
" If this is a negative number, tries to extract as many as
" possible instead of not matching.
"* RETURN VALUES:
" Original string text that got replaced, or empty string if the position does
" not exist and no replacement was done.
"******************************************************************************
let l:originalText = call('ingo#text#GetChar', [a:startPos] + a:000)
if empty(l:originalText)
return ''
endif
let [l:lnum, l:col] = a:startPos
let l:line = getline(l:lnum)
let l:len = len(l:originalText)
if setline(l:lnum, strpart(l:line, 0, l:col - 1) . a:replacement . strpart(l:line, l:col - 1 + l:len)) == 0
return l:originalText
else
return ''
endif
endfunction
function! ingo#text#RemoveVirtCol( pos, width, isAllowSmaller )
"******************************************************************************
"* PURPOSE:
" Remove a:width screen columns of text at a:pos.
"* ASSUMPTIONS / PRECONDITIONS:
" Buffer is modifiable.
"* EFFECTS / POSTCONDITIONS:
" Changes the buffer.
"* INPUTS:
" a:pos [line, virtcol]; virtcol is the 1-based screen column.
" a:width Number of screen columns.
" a:isAllowSmaller Boolean flag whether less characters can be removed if
" the end doesn't fall on a character border, or there
" aren't that many characters.
"* RETURN VALUES:
" Flag whether the position existed and removal was done.
"******************************************************************************
let [l:lnum, l:virtcol] = a:pos
if l:lnum > line('$') || a:width <= 0
return 0
endif
if l:virtcol < 1
throw 'Remove: Column must be at least 1'
endif
let l:line = getline(l:lnum)
let l:newLine = substitute(l:line, ingo#regexp#virtcols#ExtractCells(l:virtcol, a:width, a:isAllowSmaller), '', '')
if l:newLine ==# l:line
return 0
else
return setline(l:lnum, l:newLine)
endif
endfunction
" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax :