Vim: How to number paragraphs automatically and how to refer to this numbering? - vim

Let us say I have the following three paragraphs of text (separated
from each other by empty lines—number 3 and 7, here):
This is my first paragraph line 1
This is my first paragraph line 2
This is my second paragraph line 4
This is my second paragraph line 5
This is my second paragraph line 6
This is my third paragraph line 8
This is my third paragraph line 9
Question 1: How can I number these paragraphs automatically,
to obtain this result:
1 This is my first paragraph line 1
This is my first paragraph line 2
2 This is my second paragraph line 4
This is my second paragraph line 5
This is my second paragraph line 6
3 This is my third paragraph line 8
This is my third paragraph line 9
(I succeeded to do this, but only via a clumsy macro.)
Question 2: Is it possible to refer to these paragraphs? For
instance, is it possible to index a text file as answered (by Prince
Goulash and Herbert Sitz) in the earlier question, but this time
with the paragraph numbers and not the line numbers?
Thanks in advance.

Here's one way to do the ref numbers, with a pair of functions:
function! MakeRefMarkers()
" Remove spaces from empty lines:
%s/^ \+$//
" Mark all spots for ref number:
%s/^\_$\_.\zs\(\s\|\S\)/_parref_/
" Initialize ref val:
let s:i = 0
" Replace with ref nums:
%s/^_parref_/\=GetRef()/
endfunction
function! GetRef()
let s:i += 1
return s:i . '. '
endfunction
Then just do it by calling MakeRefMarkers(). It doesn't remove existing ref numbers if they're there, that would require another step. And it doesn't catch first paragraph if it's first line in file (i.e, without preceding blank line). But it does handle situations where there's more than one blank line between paragraphs.

Question One
Here is a function to enumerate paragraphs. Simply do :call EnumeratePara() anywhere in your file. The variable indent can be adjusted as you wish. Let me know if anything needs correcting or explaining.
function! EnumeratePara()
let indent = 5
let lnum = 1
let para = 1
let next_is_new_para = 1
while lnum <= line("$")
let this = getline(lnum)
if this =~ "^ *$"
let next_is_new_para=1
elseif next_is_new_para == 1 && this !~ "^ *$"
call cursor(lnum, 1)
sil exe "normal i" . para . repeat(" ", indent-len(para))
let para+=1
let next_is_new_para = 0
else
call cursor(lnum, 1)
sil exe "normal i" . repeat(" ", indent)
endif
let lnum += 1
endwhile
endfunction
Question Two
This isn't a very elegant approach, but it seems to work. First of all, here's a function that maps each line in the file to a paragraph number:
function! MapLinesToParagraphs()
let lnum = 1
let para_lines = []
let next_is_new_para = 1
let current_para = 0
while lnum <= line("$")
let this = getline(lnum)
if this =~ "^ *$"
let next_is_new_para = 1
elseif next_is_new_para == 1
let current_para += 1
let next_is_new_para = 0
endif
call add(para_lines, current_para)
let lnum += 1
endwhile
return para_lines
endfunction
So that para_lines[i] will give the paragraph of line i.
Now we can use the existing IndexByWord() function, and use MapLinesToParagraph() to convert the line numbers into paragraph numbers before we return them:
function! IndexByParagraph(wordlist)
let temp_dict = {}
let para_lines = MapLinesToParagraphs()
for word in a:wordlist
redir => result
sil! exe ':g/' . word . '/#'
redir END
let tmp_list = split(strtrans(result), "\\^\# *")
let res_list = []
call map(tmp_list, 'add(res_list, str2nr(matchstr(v:val, "^[0-9]*")))')
call map(res_list, 'para_lines[v:val]')
let temp_dict[word] = res_list
endfor
let result_list = []
for key in sort(keys(temp_dict))
call add(result_list, key . ' : ' . string(temp_dict[key])[1:-2])
endfor
return join(result_list, "\n")
endfunction
I have not tested these functions very thoroughly, but they seem to work okay, at least in your example text. Let me know how you get on!

Both problems could be solved much easier than it is suggested
by the other two answers.
1. In order to solve the first problem of numbering paragraphs,
the following two steps are ample.
Indent the paragraphs (using tabs, here):
:v/^\s*$/s/^/\t/
Insert paragraph numbering (see also my answer to
the question on substitution with counter):
:let n=[0] | %s/^\s*\n\zs\ze\s*\S\|\%1l/\=map(n,'v:val+1')
2. The second problem of creating index requires some scripting in
order to be solved by Vim means only. Below is the listing of a small
function, WordParIndex() that is supposed to be run after paragraphs
are numbered according to the first problem's description.
function! WordParIndex()
let [p, fq] = [0, {}]
let [i, n] = [1, line('$')]
while i <= n
let l = getline(i)
if l !~ '^\s*$'
let [p; ws] = ([p] + split(l, '\s\+'))[l=~'^\S':]
for w in ws
let t = get(fq, w, [p])
let fq[w] = t[-1] != p ? t + [p] : t
endfor
endif
let i += 1
endwhile
return fq
endfunction
The return value of the WordParIndex() function is the target index
dictionary. To append its text representation at the end of a buffer,
run
:call map(WordParIndex(), 'append(line("$"),v:key.": ".join(v:val,","))')

My approach would be macro based:
Yank the number "0" somehow and move to the start of the first paragraph.
Record a macro to
Indent the paragraph with >}
Paste the stored number at the correct position p
Increment the number by one with <ctrl>-a
Yank the pasted number with yiw
Move to the next paragraph with }l or /^\S
Execute the macro as many times as needed to reach the end of the document.
The method of pasting a number, incrementing it, and then reyanking it inside a macro is quite a useful technique that comes in handy whenever you need to number things. And it's simple enough to just do it in a throw-away fashion. I mainly use it for carpet logging, but it has other uses as your question demonstrates.

Related

Is there a way to get the rightmost column of a visual block selection?

Assume that virtualedit=. Consider the following text file.
1 2 1 1 1 1 1 1
1 3 1 1 1 1 1 1
1 4 1 1 1 1 1 1 1 1
1 1 1 1 7 1 1 1
If I visual-block-select lines 2 through 4 and use $ to make the selection non-rectangular, the following code will fail to find the rightmost column in the selection. More explicitly, I use the key sequence 2G^vjj$, where ^v is Control-V.
" rightmostCol becomes 15, not 19 as desired.
let rightmostCol = virtcol("'>")
Is there some other function I can call, or expression I can pass to virtcol, to programmatically get the column number of the rightmost column in my Visual Block selection?
Note that simply finding the length of the longest line in the visual block is incorrect, because the visual selection could have also been rectangular, and those cases should not be treated the same way.
Edit: If it was possible to determine whether the visual block selection is rectangular or not, that would also work.
There is no direct way to distinguish those cases through marks. One solution would be to analyze the text yourself, adding the longest line length to the leftmost visual mark position. It is important to check both marks since they could be reversed in case the selection was made towards the start.
function! RightmostVirtualColumn()
let reg_v = #v
silent normal! gv"vy
let max = 0
for line in split(getreg('v'), '\n')
let len = strlen(line)
let max = max([len, max])
endfor
let #v = reg_v
let max += min([virtcol("'<"), virtcol("'>")]) - 1
return max
endfunction
You can write a VimScript function:
function! GetRightmostCol()
let start=line("'<")
let end=line("'>")
let line=start
let len=len(getline(line))
while line<=end
let len=len<len(getline(line))?len(getline(line)):len
let line+=1
endwhile
return len
endfunction
Alternatively approach using register 0:
function! GetRightmostCol()
let lines=split(getreg(0),"\n")
let len=0
for line in lines
let len=len<len(line)?len(line):len
let line+=1
endfor
return len
endfunction
Then use the function in the assignment:
:let rightmostCol = GetRightmostCol()

Highlight entire line according to pattern match

I'm trying to get to vim or nvim to highlight the entire gui line if there is a match on the screen.
I have no idea how to begin approaching this or what to search for.
I'm able to get vim to highlght according to a pattern match, but I want it to highlight the entire gui width like it does at the bottom of the screen (in black) as shown above.
How can I achieve this?
As far as I know, it is not possible to highlight the entire line except sign feature. The following example uses #/ register value to find the last searched lines and will show the Search highlight on them.
function! s:ToggleHighlightSearchLines()
if !exists('s:id')
let s:id = localtime()
let lc = [line('.'), col('.')]
call cursor([1, 1])
let s:sc = &signcolumn
let &signcolumn = 'no'
sign define hlsLines linehl=Search
let s:sp = 0
while 1
let ln = search(#/, 'W')
if ln == 0 | break | endif
execute 'sign place ' . s:id . ' line=' . ln . ' name=hlsLines'
let s:sp += 1
endwhile
call cursor(lc)
else
while 0 < s:sp
execute 'sign unplace ' . s:id
let s:sp -= 1
endwhile
sign undefine hlsLines
let &signcolumn = s:sc
unlet s:id s:sp s:sc
endif
endfunction
command! ToggleHLSLines call s:ToggleHighlightSearchLines()

Is there any way to adjust the format of folded lines in vim?

Now my folded lines look like this:
+-- 123 lines: doSomeStuff();--------------------------
+-- 345 lines: doSomeOtherStuff();---------------------
I would like to remove everything before the actual contents of a line (+-- xxx lines:), make it more like Notepad++/Eclipse visuals way — now it's too hard to read, and I actually don't care how much lines I have under a certain fold :) So are there any commands for adjusting the format of folded lines?
Yes, foldtext function which romainl already mentioned returns a string to be displayed in a closed fold (in other words, that - what you see).
You can modify the fold function to show whatever you find interesting. For example,
function! MyFoldText() " {{{
let line = getline(v:foldstart)
let nucolwidth = &fdc + &number * &numberwidth
let windowwidth = winwidth(0) - nucolwidth - 3
let foldedlinecount = v:foldend - v:foldstart
" expand tabs into spaces
let onetab = strpart(' ', 0, &tabstop)
let line = substitute(line, '\t', onetab, 'g')
let line = strpart(line, 0, windowwidth - 2 -len(foldedlinecount))
let fillcharcount = windowwidth - len(line) - len(foldedlinecount)
return line . '…' . repeat(" ",fillcharcount) . foldedlinecount . '…' . ' '
endfunction " }}}
set foldtext=MyFoldText()
will return something similar to this
" Basic settings --------------------------------------------- {{{... 6 ...
meaning 6 lines are in the fold (including the one with the closing fold marker)
:help fold-foldtext has all the info you need.

How to resize a window to fit, taking into account only logical lines?

I'm looking to write a function that I can call from a map. The idea is to resize a window to fit the buffer contents. This isn't too difficult:
fu! ResizeWindow(vert) "{{{
if a:vert
let longest = max(map(range(1, line('$')), "virtcol([v:val, '$'])"))
exec "vertical resize " . (longest+4)
else
exec 'resize ' . line('$')
1
endif
endfu "}}}
I would, however, like the function to take logical lines into account when calculating the height (I'm not overly worried about width).
For example, a line that has wrapped (due to :set wrap) would count as two or more lines. A block of 37 lines that are folded would only count as one.
Does anyone know of a convenient way of getting this 'logical line count' without having to try and calculate it manually? If I do need to do this manually are there any other cases I'm missing that would cause a line to be represented with a potentially different number of lines?
For anyone interested, I gave up trying to find a simple solution to this. Below is the code I ended up with. It takes into account a couple of obvious edge cases; I'm sure others remain. Suggestions for improvement are very welcome.
fu! Sum(vals) "{{{
let acc = 0
for val in a:vals
let acc += val
endfor
return acc
endfu "}}}
fu! LogicalLineCounts() "{{{
if &wrap
let width = winwidth(0)
let line_counts = map(range(1, line('$')), "foldclosed(v:val)==v:val?1:(virtcol([v:val, '$'])/width)+1")
else
let line_counts = [line('$')]
endif
return line_counts
endfu "}}}
fu! LinesHiddenByFoldsCount() "{{{
let lines = range(1, line('$'))
call filter(lines, "foldclosed(v:val) > 0 && foldclosed(v:val) != v:val")
return len(lines)
endfu "}}}
fu! AutoResizeWindow(vert) "{{{
if a:vert
let longest = max(map(range(1, line('$')), "virtcol([v:val, '$'])"))
exec "vertical resize " . (longest+4)
else
let line_counts = LogicalLineCounts()
let folded_lines = LinesHiddenByFoldsCount()
let lines = Sum(line_counts) - folded_lines
exec 'resize ' . lines
1
endif
endfu "}}}

VIM incremental search, how to copy the matched string under cursor?

In VIM, you can search a specified string. Then you can press n or N to navigate next or previous match. When you press n or N, the cursor will be moved to the matched text. My question is, how to quickly copy the matched text under cursor?
Edit:
What I need is the current match under cursor, not all matches in the document.
You want to execute the following
y//e
Overview:
The basic idea is after you search or press n or N the cursor will be at the beginning of the matched text. Then you yank to the end of the last search.
Explanation
y will yank from the current position through the following motion
// searches using the last search pattern.
//e The e flag will position the cursor at the end of the matched text
As a word of warning this will change the current search pattern, because it adds the /e flag. Therefore following n and/or N will move the cursor to the end of the match.
This is very similar to the following post.
One can write a function extracting the match of the last search pattern
around the cursor, and create a mapping to call it.
nnoremap <silent> <leader>y :call setreg('"', MatchAround(#/), 'c')<cr>
function! MatchAround(pat)
let [sl, sc] = searchpos(a:pat, 'bcnW')
let [el, ec] = searchpos(a:pat, 'cenW')
let t = map(getline(sl ? sl : -1, el), 'v:val."\n"')
if len(t) > 0
let t[0] = t[0][sc-1:]
let ec -= len(t) == 1 ? sc-1 : 0
let t[-1] = t[-1][:matchend(t[-1], '.', ec-1)-1]
end
return join(t, '')
endfunction
The function above determines the starting and ending positions of the match
and carefully takes out the matching text, correctly handling multiline
patterns and multibyte characters.
Another option is to create text object mappings (see :help text-object) for
operating on the last search pattern match under the cursor.
vnoremap <silent> i/ :<c-u>call SelectMatch()<cr>
onoremap <silent> i/ :call SelectMatch()<cr>
function! SelectMatch()
if search(#/, 'bcW')
norm! v
call search(#/, 'ceW')
else
norm! gv
endif
endfunction
To copy the current match using these mappings, use yi/. As for other text
objects, it is also possible, for example, to visually select it using vi/,
or delete it using di/.
Press y and the text under the cursor will be copied to the unamed default register, then you can press p to paste.
I guess this should do it:
command! -register CopyExactMatchUnderCursor call s:CopyExactMatchUnderCursor('<reg>')
function! s:CopyExactMatchUnderCursor(reg)
let cpos = getpos('.')
let line = cpos[1]
let idx = cpos[2] - 1
let txt = getline(line)
let mend = matchend(txt, #/, idx)
if mend > idx
let sel = strpart(txt, idx, mend - idx)
let reg = empty(a:reg) ? '"' : a:reg
execute 'let #' . reg . ' = sel . "\n"'
end
endfunction
It grabs the cursor position first with getpos then searches for the end of the match (beginning with the column where the cursor is using matchend) and then returns the substring in the register provided - by default " is used.
All the necessary methods were basically in #eckes answer.

Resources