mirror of
https://github.com/inkarkat/vim-ingo-library.git
synced 2026-05-29 11:18:51 +02:00
700aec39d2
System-info: Ubuntu 24.04.3 LTS, x86_64 Platform-info: Vim 9.1.2108
447 lines
17 KiB
VimL
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 :
|