Vim search in C/C++ code lines - vim

Is there any way to search a string in a C/C++ source file while skipping commented lines?

This is an intriguing question.
I think #sixtyfootersdude has the right idea -- let Vim's syntax highlighting tell you what's a comment and what's not, and then search for matches within the non-comments.
Let's start with a function that mimics Vim's built-in search() routine, but also provides a "skip" parameter to let it ignore some matches:
function! SearchWithSkip(pattern, flags, stopline, timeout, skip)
"
" Returns true if a match is found for {pattern}, but ignores matches
" where {skip} evaluates to false. This allows you to do nifty things
" like, say, only matching outside comments, only on odd-numbered lines,
" or whatever else you like.
"
" Mimics the built-in search() function, but adds a {skip} expression
" like that available in searchpair() and searchpairpos().
" (See the Vim help on search() for details of the other parameters.)
"
" Note the current position, so that if there are no unskipped
" matches, the cursor can be restored to this location.
"
let l:matchpos = getpos('.')
" Loop as long as {pattern} continues to be found.
"
while search(a:pattern, a:flags, a:stopline, a:timeout) > 0
" If {skip} is true, ignore this match and continue searching.
"
if eval(a:skip)
continue
endif
" If we get here, {pattern} was found and {skip} is false,
" so this is a match we don't want to ignore. Update the
" match position and stop searching.
"
let l:matchpos = getpos('.')
break
endwhile
" Jump to the position of the unskipped match, or to the original
" position if there wasn't one.
"
call setpos('.', l:matchpos)
endfunction
Here are a couple of functions that build on SearchWithSkip() to implement syntax-sensitive searches:
function! SearchOutside(synName, pattern)
"
" Searches for the specified pattern, but skips matches that
" exist within the specified syntax region.
"
call SearchWithSkip(a:pattern, '', '', '',
\ 'synIDattr(synID(line("."), col("."), 0), "name") =~? "' . a:synName . '"' )
endfunction
function! SearchInside(synName, pattern)
"
" Searches for the specified pattern, but skips matches that don't
" exist within the specified syntax region.
"
call SearchWithSkip(a:pattern, '', '', '',
\ 'synIDattr(synID(line("."), col("."), 0), "name") !~? "' . a:synName . '"' )
endfunction
Here are commands that make the syntax-sensitive search functions easier to use:
command! -nargs=+ -complete=command SearchOutside call SearchOutside(<f-args>)
command! -nargs=+ -complete=command SearchInside call SearchInside(<f-args>)
That was a long way to go, but now we can do stuff like this:
:SearchInside String hello
That searches for hello, but only within text that Vim considers a string.
And (finally!) this searches for double everywhere except comments:
:SearchOutside Comment double
To repeat a search, use the #: macro to execute the same command repeatedly, like pressing n to repeat a search.
(Thanks for asking this question, by the way. Now that I've built these routines, I expect to use them a lot.)

This pattern searches for a string that is not preceded by the two C++ commenting conventions. I've also excluded '*' as the first non-whitespace character, as that's a common convention for multi-line comments.
/\(\(\/\*\|\/\/\|^\s*\*[^/]\).*\)\#<!foo
Only the first and fourth foo are matched.
foo
/* foo
* baz foo
*/ foo
// bar baz foo
Putting \v at the beginning of the pattern eliminates a bunch of backslashes:
/\v((\/\*|\/\/|^\s*\*[^/]).*)#<!foo
You can bind a hotkey to this pattern by putting this in your .vimrc
"ctrl+s to search uncommented code
noremap <c-s> <c-o>/\v((\/\*\|\/\/\|^\s*\*[^/]).*)#<!

Not sure if this is helpful but when you type :syn it has all the formatting that is used in your file type. Maybe you can refer to that somehow. You could say something like:
map n betterN
function betterN{
n keystroke
while currentLine matches comment class
do another n keystroke
}

Here is how I would proceed:
Delete all C/C++ comments (using the replace command %s)
Proceed to the search using regular search command /
Set a mark at the position using m a (to set the mark "a")
Undo the deletion of the comments using u
Jump to the mark "a" using ``a`
Eventually deleting the mark using delm a (it would be overwritten in the case you don't delete it, so no big deal)
Of course you can do that in one big operation/function. I do not master Vim scripting good enough to give an example of that though.
I admit my solution is a bit "lazy" (and you can probably do it way better) but that's all I came to.
Hope it helps.

Related

How to run commands against block-wise visual mode selection?

I have a testing file with the content:
var a = f
ff
fff
Then I moved the cursor on the f character in line 1, ctrl+v selected the two f below it (■ means selection).
var a = ■
■f
■ff
I want to change the text to this:
var a = "f"
"ff"
"fff"
So I executed this command:
:normal i"ctrl+vEscA"
But the quotes were added to the whole line. Is it possible to do operations on only block-wise visual selection (not the whole line)? Note that this example was made only for discussing Vim skills. I'm not trying to solving any problems, just want to learn Vim skills.
The problem is that all :-commands can take linewise range only. Recall that after pressing : in Visual mode you got :'<,'>_ which is a hint strong enough (see :h mark-motions in case you don't remember about backtick vs. single-quote).
So you can't process visually selected text directly but have to yank it into register first.
One solution is to yank-put the text into the lines of its own. Then you can apply your command(s) and move the new text where it should be.
This is how it's done in that "vis" plugin mentioned in the comment by #phd and the linked answer. But it's so big and bloated that we'd better implement such functionality ourselves.
" apply command to a visual selection
" a:cmd - any "range" command
" a:mods - :h command-modifiers
" a:trim - true if we need to trim trailing spaces from selection text
function s:block(cmd, mods, trim) abort
" save last line no
let l:last = line('$')
" yank selected text into the register "9"
silent normal! gv"9y
" put it at buffer's end
call append(l:last, getreg(9, 1, 1))
" trim trailing spaces
if a:trim
execute 'keepj keepp' l:last..'+,$s/\s\+$//e'
endif
" apply our command
" note: we assume that the command never enters Visual mode
execute a:mods l:last..'+,$' a:cmd
" get the changed text back into the register "9"
call setreg(9, getline(l:last + 1, '$'), visualmode())
" clean up
call deletebufline('%', l:last + 1, '$')
" replace the selection
" note: the old text goes into the register "1", "1" into "2", and so on.
" old register "9" is lost anyway, see :h v_p
silent normal! gv"9p
endfunction
" our interface is a user command
command! -bang -range -nargs=1 -complete=command B call s:block(<q-args>, <q-mods>, <bang>0)
Now you can select your block (do not forget about extending it until lines' ends!), and execute:
:'<,'>B! normal! i"^CA"
Note: ^C stands for Ctrl-VCtrl-C
On possible solution:
:'<,'>norm $ciw"ctrl-v ctrl-r""
OBS: Ctrl-v Ctrl-r should be typed literally
Another solution:
:'<,'>s/\w\+$/"&"

vim search and replace only on non-commented lines

I am trying to search and replace only in non-commented line.
By non commented line, I mean lines that are not greyed out by the vim syntax highlighting.
For example:
/*
...
DO NOT REPLACE
....
*/
Should not be searched and replaced.
My problem is specific to C++ but could be expanded to any language if there is a way to use the "syntax engine" of vim.
Edit : The link is only about searching, nothing about replacing.
I've finally done it myself using Bill Odom code as a base :
function Repl(pattern, patternSub)
" Move to top of the file
normal 1G
" Search Pattern, no wrap
while search(a:pattern, "W", "", "") > 0
" If found pattern is a comment, skip
if eval(synIDattr(synID(line("."), col("."), 0), "name") =~? "Comment")
continue
endif
" If found pattern is a string, skip
if eval(synIDattr(synID(line("."), col("."), 0), "name") =~? "String")
continue
endif
" Replace pattern by it's substitute
exec '.s/'.a:pattern.'/'.a:patternSub.'/'
" Restore cursor position
normal ``
endwhile
endfunction
command! -nargs=+ -complete=command Repl call Repl(<args>)
It does not replace any occurence of pattern which is a Comment or a String.
It actually is language independent.
You have to use it this way ( eg: removing space after all non-commented parenthesis ):
:Repl '( *','('

Escape characters during paste in vim

I copy stuff from output buffers into C++ code I'm working on in vim.
Often this output gets stuck into strings. And it'd be nice to be able to escape all the control characters automatically rather than going back and hand editing the pasted fragment.
As an example I might copy something like this:
error in file "foo.dat"
And need to put it into something like this
std::string expected_error = "error in file \"foo.dat\""
I'm thinking it might be possible to apply a replace function to the body of the last paste using the start and end marks of the last paste, but I'm not sure how to make it fly.
UPDATE:
Joey Mazzarelli sugested using
`[v`]h:%s/\%V"/\\"/g
after a paste.
Since no explaination was given for what that was going and I initially found it a bit terse, but hard to explain in the comments I thought I'd put an explaination of what I think that does here:
`[ : Move to start of last paste
v : Start visual mode
`] : Move to end of last paste
h : adjust cursor one position left
:% : apply on the lines of the selection
s/ : replace
\%V : within the visual area
" : "
/ : with
\\" : \"
/g : all occurrences
This seems like a good approach, but only handles the one character, ", I'd like it to handle newlines, tabs, and other things that might be expected to fall in text. (Probably not general unicode though) I understand that may not have been clear in the problem definition.
Here are a couple of vimscript functions that should do what you want.
EscapeText() transforms arbitrary text to the C-escaped equivalent. It converts newline to \n, tab to \t, Control+G to \a, etc., and generates octal escapes (like \o032) for special characters that don't have a friendly name.
PasteEscapedRegister() escapes the contents of the register named by v:register, then inserts it into the current buffer. (The register is restored when the function returns, so the function can be called repeatedly without escaping the register contents multiple times.)
There are also a couple of key mappings included to make PasteEscapedRegister() easy to use interactively. <Leader>P pastes escaped register contents before the cursor position, and <Leader>p pastes after. Both can be prefixed with a register specification, like "a\P to paste the escaped contents of register a.
Here's the code:
function! EscapeText(text)
let l:escaped_text = a:text
" Map characters to named C backslash escapes. Normally, single-quoted
" strings don't require double-backslashing, but these are necessary
" to make the substitute() call below work properly.
"
let l:charmap = {
\ '"' : '\\"',
\ "'" : '\\''',
\ "\n" : '\\n',
\ "\r" : '\\r',
\ "\b" : '\\b',
\ "\t" : '\\t',
\ "\x07" : '\\a',
\ "\x0B" : '\\v',
\ "\f" : '\\f',
\ }
" Escape any existing backslashes in the text first, before
" generating new ones. (Vim dictionaries iterate in arbitrary order,
" so this step can't be combined with the items() loop below.)
"
let l:escaped_text = substitute(l:escaped_text, "\\", '\\\', 'g')
" Replace actual returns, newlines, tabs, etc., with their escaped
" representations.
"
for [original, escaped] in items(charmap)
let l:escaped_text = substitute(l:escaped_text, original, escaped, 'g')
endfor
" Replace any other character that isn't a letter, number,
" punctuation, or space with a 3-digit octal escape sequence. (Octal
" is used instead of hex, since octal escapes terminate after 3
" digits. C allows hex escapes of any length, so it's possible for
" them to run up against subsequent characters that might be valid
" hex digits.)
"
let l:escaped_text = substitute(l:escaped_text,
\ '\([^[:alnum:][:punct:] ]\)',
\ '\="\\o" . printf("%03o",char2nr(submatch(1)))',
\ 'g')
return l:escaped_text
endfunction
function! PasteEscapedRegister(where)
" Remember current register name, contents, and type,
" so they can be restored once we're done.
"
let l:save_reg_name = v:register
let l:save_reg_contents = getreg(l:save_reg_name, 1)
let l:save_reg_type = getregtype(l:save_reg_name)
echo "register: [" . l:save_reg_name . "] type: [" . l:save_reg_type . "]"
" Replace the contents of the register with the escaped text, and set the
" type to characterwise (so pasting into an existing double-quoted string,
" for example, will work as expected).
"
call setreg(l:save_reg_name, EscapeText(getreg(l:save_reg_name)), "c")
" Build the appropriate normal-mode paste command.
"
let l:cmd = 'normal "' . l:save_reg_name . (a:where == "before" ? "P" : "p")
" Insert the escaped register contents.
"
exec l:cmd
" Restore the register to its original value and type.
"
call setreg(l:save_reg_name, l:save_reg_contents, l:save_reg_type)
endfunction
" Define keymaps to paste escaped text before or after the cursor.
"
nmap <Leader>P :call PasteEscapedRegister("before")<cr>
nmap <Leader>p :call PasteEscapedRegister("after")<cr>
This might at least get you started...
After pasting it in:
`[v`]h:%s/\%V"/\\"/g
You can obviously map that to something easier to type.
While Joeys solution looks like it might be extensible to cover all the cases that I need, I thought I'd share my partial solution using vims python integration (Since I'm more familiar at python than vim script)
# FILE : tocstring.py
import vim
def setRegister(reg, value):
vim.command( "let #%s='%s'" % (reg, value.replace("'","''") ) )
def getRegister(reg):
return vim.eval("#%s" % reg )
def transformChar( map, c):
if c in map:
return map[c]
return c
def transformText( map, text ):
return ''.join( [ transformChar(map,c) for c in text ] )
cmap={}
cmap["\\"]="\\\\"
cmap["\n"]="\\n"
cmap["\t"]=r"\\t"
cmap['"']="\\\""
def convertToCString( inregister, outregister ):
setRegister(outregister, transformText( cmap, getRegister(inregister) ) )
Then in my .vimrc or other conf file I can put
# FILE cpp.vim
python import tocstring
# C-Escape and paste the currently yanked content
nmap <Leader>P :python tocstring.convertToCString("#","z")<CR>"zP
# C-Escape and paste the current visual selection
vmap <Leader>P "zd:python tocstring.convertToCString("z","z")<CR>"zP
It would be nice if I could the first function to work so that "a\P pasted the transformed contents of the "a" register, and I assume this is doable using v:register somehow, but it escapes me.
A version of this that works in the same way as Joeys solution could be crafted as
nmap <Leader>P `[v`]"zd:python tocstring.convertToCString("z","z")<CR>"zP
Acknowledgement : This uses code from Can you access registers from python functions in vim for interacting with registers from vims python
for Java/JavaScript type of escaping one can use json_encode
nmap <leader>jp :call setreg('e', json_encode(#+))\| normal "ep<CR>
json_encode(#+) - json encode content of register + (mapped to clipboard)
setreg('e',...) - write it to register e
normal "ep - paste content of register e

Identifying Identical Blocks of Code

Say I have multiple blocks of the following code in a file (spaces is irrelavent):
sdgfsdg dfg
dfgdfgf ddfg
dfgdfgdfg dfgfdg
How do you find/highlight all the occurrences?
What I ideally want to do is to visually select the code block and then press search to find all occurrences.
Maybe you should look at :
Search for visually selected text
I've taken it from here
Try this. Include this script somewhere in your runtimepath (see :help runtimepath). A simple option would be to put it in your vimrc. Visually select the thing you want to search for and press ,/ (the comma key and then the forward-slash key).
" Search for other instances of the current visual range
" This works by:
" <ESC> Cancel the visual range (it's location is remembered)
" / Start the search
" <C-R>= Insert the result of an expression on
" the search line (see :help c_CTRL-R_= )
" GetVisualRange()<CR> Call the function created below
" <CR> Run the search
vmap ,/ <ESC>/<C-R>=GetVisualRange()<CR><CR>
" Create the function that extracts the contents of the visual range
function! GetVisualRange()
" Get the start and end positions of the current range
let StartPosition = getpos("'<")
let EndPosition = getpos("'>")
" Prefix the range with \V to disable "magic"
" See :help \V
let VisualRange = '\V'
" If the start and end of the range are on the same line
if StartPosition[1] == EndPosition[1]
" Just extract the relevant part of the line
let VisualRange .= getline(StartPosition[1])[StartPosition[2]-1:EndPosition[2]-1]
else
" Otherwise, get the end of the first line
let VisualRange .= getline(StartPosition[1])[StartPosition[2]-1:]
" Then the all of the intermediate lines
for LineNum in range(StartPosition[1]+1, EndPosition[1]-1)
let VisualRange .= '\n' . getline(LineNum)
endfor
" Then the start of the last line
let VisualRange .= '\n' . getline(EndPosition[1])[:EndPosition[2]-1]
endif
" Replace legitimate backslashes with double backslashes to prevent
" a literal \t being interpreted as a tab
let VisualRange = substitute(VisualRange, '\\[nV]\#!', '\\\\', "g")
" Return the result
return VisualRange
endfunction
The text being searched for is stored in the / register. You can't yank or delete directly into this register, but you can assign to it using `let'.
Try this:
Use visual mode to highlight the code you want to search for
Type "ay to yank that highlighted selection into register a
Type :let #/ = #a to copy register a into the search register /
At this point, all code matching your selection will be highlighted, and you can navigate through occurrences using n/N just as you would a regular search.
Of course, you can use any temporary register instead of a. And it shouldn't be too difficult to get this command sequence mapped for easy use.
Quick and dirty partial solution:
:set hlsearch
*
The hlsearch option (on by default in some vim configs, but I always turn it off) makes vim highlight all found instances of the current search. Pressing * in normal mode searches for the word under the cursor. So this will highlight all instances of the word under the cursor.

How to "apply" backspace characters within a text file (ideally in vim)

I have a log file with backspace characters in it (^H). I'm looking through the file in Vim and it can be quite hard to see what's going on.
Ideally I'd like to be able to "apply" all the ^H on a given line/range so that I can see the final result.
I'd much rather do this within Vim on a line-by-line basis, but a solution which converts the whole file is better than nothing.
Turn on the 'paste' option (using :set paste), and then press dd i <CTRL-R> 1 <ESC> on each line that you want to apply the backspaces to. This also works if you delete multiple lines, or even the whole file.
The key here is that you are using <CTRL-R> 1 in insert mode to 'type out' the contents of register 1 (where your deleted lines just got put), and 'paste' option prevents Vim from using any mappings or abbreviations.
I googled this while trying to remember the command I had used before to `apply' backspaces, and then I remembered it: col -b - here is the manpage. (It does a little more and comes from BSD or more exactly AT&T UNIX as the manpage says, so if you are on Linux you may need to install an additional package, on debian its in bsdmainutils.)
Simplistic answer:
:%s/[^^H]^H//g
where ^^H is:
Literal ^ character
Ctrl-V Ctrl-H
and repeat it couple of times (until vim will tell you that no substitutions have been made
If you want without repetition, and you don't mind using %!perl:
%!perl -0pe 's{([^\x08]+)(\x08+)}{substr$1,0,-length$2}eg'
All characters are literal - i.e. you don't have to do ctrl-v ... anywhere in above line.
Should work in most cases.
All right, here is a bare-metal solution.
Copy this code into a file named crush.c:
#include <stdio.h>
// crush out x^H sequences
// there was a program that did this, once
// cja, 16 nov 09
main()
{
int c, lc = 0;
while ((c = getchar()) != EOF) {
if (c == '\x08')
lc = '\0';
else {
if (lc)
putchar(lc);
lc = c;
}
}
if (lc)
putchar(lc);
}
Compile this code with your favorite compiler:
gcc crush.c -o crush
Then use it like this to crush out those bothersome sequences:
./crush <infilename >outfilename
Or use it in a pipeline ("say" is a speech-to-text app on the Mac)
man date | ./crush | say
You can copy crush to your favorite executable directory (/usr/local/bin, or some such) and then reference it as follows
man date | crush | say
Just delete all occurrences of .^H (where . is the regex interpretation of .):
:s/.^H//g
(insert ^H literally by entering Ctrl-V Ctrl-H)
That will apply to the current line. Use whatever range you want if you want to apply it to other lines.
Once you done one :s... command, you can repeat on another line by just typing :sg (you need to g on the end to re-apply to all occurrences on the current line).
How about the following function? I've used \%x08 instead of ^H as it's easier to copy and paste the resulting code. You could type it in and use Ctrl-V Ctrl-H if you prefer, but I thought \%x08 might be easier. This also attempts to handle backspaces at the start of the line (it just deletes them).
" Define a command to make it easier to use (default range is whole file)
command! -range=% ApplyBackspaces <line1>,<line2>call ApplyBackspaces()
" Function that does the work
function! ApplyBackspaces() range
" For each line in the selected lines
for index in range(a:firstline, a:lastline)
" Get the line as a string
let thisline = getline(index)
" Remove backspaces at the start of the line
let thisline = substitute(thisline, '^\%x08*', '', '')
" Repeatedly apply backspaces until there are none left
while thisline =~ '.\%x08'
" Substitute any character followed by backspace with nothing
let thisline = substitute(thisline, '.\%x08', '', 'g')
endwhile
" Remove any backspaces left at the start of the line
let thisline = substitute(thisline, '^\%x08*', '', '')
" Write the line back
call setline(index, thisline)
endfor
endfunction
Use with:
" Whole file:
:ApplyBackspaces
" Whole file (explicitly requested):
:%ApplyBackspaces
" Visual range:
:'<,'>ApplyBackspaces
For more information, see:
:help command
:help command-range
:help function
:help function-range-example
:help substitute()
:help =~
:help \%x
Edit
Note that if you want to work on a single line, you could do something like this:
" Define the command to default to the current line rather than the whole file
command! -range ApplyBackspaces <line1>,<line2>call ApplyBackspaces()
" Create a mapping so that pressing ,b in normal mode deals with the current line
nmap ,b :ApplyBackspaces<CR>
or you could just do:
nmap ,b :.ApplyBackspaces<CR>
Here's a much faster Awk filter that does the same:
#!/usr/bin/awk -f
function crushify(data) {
while (data ~ /[^^H]^H/) {
gsub(/[^^H]^H/, "", data)
}
print data
}
crushify($0)
Note that where ^^H appears, the first caret in ^^H is a caret (shift-6) and the second caret with H is entered (into vim) by typing CTRL-v CTRL-H
Here's a Bash-based filter you can use to process the whole file:
#!/bin/bash
while read LINE; do
while [[ "$LINE" =~ '^H' ]]; do
LINE="${LINE/[^^H]^H/}"
done
echo "$LINE"
done
Note that where ^H appears, it is entered into vim using CTRL-v CTRL-h, and the ^^H is entered as SHIFT-6 CTRL-v CTRL-h.

Resources