0

I have many entities in my java project like this:

public class MyCar {
// The brand of the car
private String brand;
// The time owner bought it
private Date obtainTime;
// The price in dollar
private Double price;
}

Now I want to replace them with this format:

public class MyCar {
@ApiModelProperty("The brand of the car")
private String brand;
@ApiModelProperty("The time owner bought it")
private Date obtainTime;
@ApiModelProperty("The price in dollar")
private Double price;
}

Is there any way to do this in my whole project, which means to search and replace in whole directory recursively?

3
  • Welcome to Vi and Vim! Can you please clarify which parts of the line are fixed and which can vary? Is the field name always field (or always the same), or do you want to handle many different fields? Is the type always String? Are they always private? Is the comment a fixed string, such as always field comment, or is it the point to grab whatever is there and pass it as an argument to @ApiModelProperty? Can the indentation vary between files? Please edit the question to clarify what exactly you have in mind...
    – filbranden
    May 12 at 1:06
  • 1
    Thanks for your questions. the comment, type and field are variable. I've updated my question to make it more clear.
    – yz zheng
    May 12 at 5:12
  • Unless those are the only comments or it's always the format //<comment><newline>private <type> <field>; it could be hard to automatically target all the right places. But the answers provide some solid starting points. (And welcome!)
    – D. Ben Knoble
    May 12 at 14:59
2

You have a couple of good answers here already, but the fact that both solutions use the shell implies that you can't do this from inside Vim, which isn't true! Here are two possible Vim-native solutions:

Use vimgrep

  1. Add all the lines to change to your quickfix:

     :vimgrep /\v\/\/\s*.*\n(private|public)(\s+\k+){2};/ **/*.java
    
  2. For each line, make the edit with normal mode commands:

     :cdo norm!caw@ApiModelProperty("^[A")
                                     ^^
                                     This is a literal ESC character.
                                     Enter it by typing <Ctrl-V><Esc>
    

You could of course use a :substitute) command to make the edit in step 2. I just didn't fancy writing out the regular expression three times.

Use the arglist

  1. Set your arglist to contain all the .java files:

     :args **/*.java
    
  2. Make a substitution for every match in every file:

     :argdo %s/\v\/\/\s*(.*)\ze\n(private|public)(\s+\k+){2};/@ApiModelProperty("\1")/ | up
    

If you have the 'hidden' option set, you don't need the | up at the end of the second command. This is only required to write each file if there are changes before switching to the next file, otherwise Vim will complain.

7
  • I'll have you know that I implied no such thing, SIR! ;) My point is I prefer to use the best tool for the job and when it comes to bulk file operations shell is the clear winner over Vim, generally speaking. (That being said, I fully expected that votes for my answer would be hard to come by...especially if pure Vim solution(s) got posted.) Also see my conversation with DBK under my answer where I mention keeping current work separate from (potentially hundreds of) these project files (e.g. swamping your arglist all of a sudden).
    – B Layer
    May 12 at 18:37
  • @BLayer I was super careful not to say that you implied that Vim can't do this: it was the existence of two different shell-based solutions that caused said implication. I simply could not allow people less-well-versed in Vim than yourself to come to incorrect conclusions about Vim's capabilities!
    – Rich
    May 13 at 9:08
  • I don't know. I'd argue that if you had been "super careful" you would have chosen less ambiguous words than "the fact that they both use the shell implies...". :P :) :)
    – B Layer
    May 13 at 10:28
  • 1
    I mean, everyone knows Vim is always the best tool for the job, right? ;-)
    – filbranden
    May 13 at 15:26
  • 2
    @ Rich I suppose that's sufficiently mollifying...just barely. @filbranden Fanboy claptrap!
    – B Layer
    May 13 at 17:31
1

I have two shell functions in my ~/.zshrc that are quite useful for this (I think they should work for bash as well, but I didn't test, and with minimal modification it should even work in standard POSIX shells):

# "ag edit" and "grep edit".
age() {
    IFS=$'\n' files=($(ag "$@" | cut -d: -f1 | sort -u))
    [[ -z "$files" ]] && return 1
    vim \
        +':silent! /\v'"${@[-1]/\//\\/}" \
        +':silent! tabdo :1 | normal! n' \
        +':tabfirst' \
        -p $files
}
grepe() {
    IFS=$'\n' files=($(ag "$@" | cut -d: -f1 | sort -u))
    [[ -z "$files" ]] && return 1
    vim \
        +':silent! /\v'"${@[-1]/\//\\/}" \
        +':silent! tabdo :1 | normal! n' \
        +':tabfirst' \
        -p $files
}

What I usually do is ag 'String field' first to confirm that it matches what I expect, and then modify this to age 'String field' to open all the files in Vim tabs (vim -p opens every file in a new tab).

This sets the search pattern in Vim to whatever you typed on the commandline and goes to the first match, but this isn't perfect as the Vim syntax isn't the same as the syntax grep and ag use, so complex patterns tend to break. It's usually "good enough" for simpler patterns.

Then :%s/.../ whatever you want, and if you're confident you've got it right run :tabdo :%s/.../ so that it's run on all tabs. :wqa and verify that it's all correct.

You can use ripgrep (rg) instead of the_silver_searcher (ag) as well; actually, ag an alias for rg for me, it's just that I was already so used to typing ag when I switched from ag to rg that it made sense to keep using this command.

You don't need to use tabs, you can use buffers and :bufdo as well. I just like to use tabs.

There are a myriad of other ways to do the same, B Layer's answer already mentioned some. But I find that my custom age is super helpful, for this use case and many others, and probably the "custom command" I use the most.

3
  • Actually, zsh-ism is a bad characterization. That's pretty close to a legal construct in Bash but something's not quite right (syntax error). I'll have to look closer.
    – B Layer
    May 12 at 8:52
  • 1
    You might like the git-jump script in the contrib directory of git
    – D. Ben Knoble
    May 12 at 10:30
  • 1
    The only reason I started using rg is because of the -g flag @BLayer, which allows including or excluding files by glob pattern, which is pretty handy every now and then. Outside of that, I find there's little or no difference for me. May 12 at 18:07
0

filbranden asks some good questions but since I've already got this ready we can use it as a jump off point. Hopefully nothing more than tweaking of the substitution command will be needed.

I'd recommend that you not try to do this from within an instance of Vim but instead leverage the power of a shell (Bash) PLUS Vim. (If you're on Windows then this will work with Cygwin or WSL. If you don't have either, then, well...we're out of luck.)

This is one way to do it...

while IFS= read -r f; do echo "$f"; vim -e -c '%s#\v^// (field comment)#@ApiModelProperty("\1")#g' -c 'x' "$f"; done < <(find {DIR} -name \*.java)

For easier reading here's it broken out into multiple lines...

while IFS= read -r f; do
    echo "Processing $f ..."
    vim -e -c '%s#\v^// (field comment)#@ApiModelProperty("\1")#g' -c 'x' "$f"
done < <(find {DIR} -name \*.java)

Change {DIR} to the path of the root directory of your project. Or cd to that dir before running the command and just use . in place of {DIR}.

The while loop is in a form that handles funky file names pretty well. It iterates over the output of find. The find command will recursively search for files with extension java. Each of these will be passed to an invocation of Vim in "batch mode" (that's what the -e is for). The -c flag tells Vim to run what follows as a regular Ex command and I'm doing that with a standard substitution and again with x which writes the file, if necessary, and quits editing the current file.

Note: A lot of people might opt to use sed -i here instead of Vim but actually Vim is better suited for this task since it modifies the file without having to clumsily make a copy of the target file like sed does.

BTW, if you really wanted to you could call this from within Vim but you might have to make some changes to the quoting or add some escaping.

11
  • 1
    But... :args `find …` and then :argdo %substitute…
    – D. Ben Knoble
    May 12 at 2:46
  • Why? For pretty much any multi-file operation I'm going to ctrl-z out of Vim and be more effective and have more capabilities using the shell than in Vim. This kind of stuff is the shell's strength. I use the best tool for the job. (It's a coincidence that Vim fits nicely into this particular solution. Vim isn't a file system utility.)
    – B Layer
    May 12 at 7:56
  • Also, why would one want to swamp their arg list and buffer list with all the files to be changed? What if there are hundreds of java files? (I assume that's what happens with your command...correct me if I'm wrong.) I'd prefer to keep separate whatever I was working on before from this bulk file change.
    – B Layer
    May 12 at 8:06
  • 1
    ed is identical to vim -e, I believe. And unless one sends null-delimited lines instead of newline-delimited ones to xargs it can't handle all legal filenames so my preference is almost always to use while and process substitution unless using a subshell is problematic. Or, if it the problem can really benefit from some parallelism, I love parallel. :) It's like xargs on steroids!
    – B Layer
    May 12 at 10:43
  • 1
    You mean ex :) but those are fair points. I tend to use xsrgs -0. I’ve seen parallel once or twice but not really used it. Anyway, cheers :)
    – D. Ben Knoble
    May 12 at 10:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.