I use grep.vim to grep in multiple files within vim. It usually works pretty well, but I recently needed a little more freedom in how I specify the files in which I want to search, and found that it can’t do what I want. I looked at the code and found that modifying it to do what I want would be hard with the current design, so I decided to create my own plugin.

The plugin I’m going to create is going to be very simple. It is going to provide simple greping functionality with a similar UI to grep.vim, but will allow more freedom on filtering which files to search.

VimL - Vimscript

VimL (or Vimscript) is the language used by Vim for mostly everything. The .vimrc file used for configuring Vim is written in VimL. VimL is the only way to write a plugin for Vim, so it is necessary to get familiar with it before we start.

Introduction

If you have written a .vimrc file, you already know how to create a comment in VimL:

1
" This is a comment in VimL

A command that will be useful for debugging while writing our plugin is echomsg. This command will output a message to the user, and it will also save it to the message history. The command can be used like this:

1
echomsg "This is a message"

We can execute any command from within a Vim session by preceding it with :, for example:

1
:echomsg "This is a message"

To see the message history we can use:

1
:messages

We can get more information about any command with the help command:

1
:help messages

Variables

VimL is a scripting programming language, so it has variables, conditions, loops, etc. Let’s explore them.

To create a variable, we use the let command:

1
let some_variable = "some value"

If we want to modify the value of the variable, we have to use the let command too:

1
2
3
4
5
" This is an error:
some_variable = "some value"

" This is correct:
let some_variable = "another value"

Variables can be accessed by their name:

1
echomsg some_variable

We can also access vim options, by preceding them with &:

1
echomsg &number

The number option tells us if line numbers are enabled in the current buffer. If line numbers are enabled, it will print 1, otherwise it will print 0. We can also set options programatically using let:

1
let &number = 1

The code above enables line numbers in the current buffer.

Conditions

As most other programming languages, VimL uses if for writing conditionals:

1
2
3
4
5
6
7
if 0
  echomsg "some message"
elseif 1
  echomsg "some other message"
else
  echomsg "last message"
endif

In the example above some other message will be printed. As expected, 0 is false and 1 is true. VimL always uses numbers to evaluate truthness of a value. If the value is 0, then it’s false, any other number is true. If a string is used in a condition, it will be converted to a number and evaluated using the same rules:

1
2
3
4
5
6
0 -> false
1 -> true
20 -> true
"hello" -> false
"0 something" -> false
"7 dwarfs" -> true

You can also use comparisons on conditionals. For the most part they work as expected:

1
2
3
4
5
6
7
8
9
10
11
if 22 > 10
  echomsg "yes"
endif

if 22 == 22
  echomsg "yes"
endif

if "abc" != "def"
 echomsg "yes"
endif

All these conditions evaluate to true. One thing to keep in mind is that string comparisons might or might not be case-sensitive depending on user settings. Because of this, we should always use ==? (case-insensitive comparison) and ==# (case-sensitive comparison) when comparing strings.

Functions

Functions are defined by using the function keyword:

1
2
3
function Something()
  echomsg "Something"
endfunction

Functions should always start with a capital letter. To run it we use call:

1
call Something()

Functions are more useful when they take arguments and return values:

1
2
3
function Hello(name)
  return "Hello " . a:name
endfunction

The function receives a single argument called name. Inside the function body it is accessed by preceding it with a:. This is the scope of the variable, there are different scopes a variable can have. Another new thing is the concatenation operator ., it adds two strings together.

We can call this function:

1
call Hello("world")

Functions can also be used in expressions:

1
let output = Hello("world")

output now contains the string Hello world.

Commands

Something that we will need if we are creating a plugin, is to define commands. Commands can be used by the user from normal mode, for example help is a built in command:

1
:help messages

The signature for defining a command is:

1
:command {attributes} {name} {replacement}

I’m not going to go in a lot of depth into it, because you can get good information with :help command, but this is an example:

1
:command -nargs=0 Hello echo "Hello everybody!"

What -nargs=0 means is that this command doesn’t expect any arguments. If you give it any arguments, an error will be shown. All the command does is echo Hello everybody! when the Hello command is used.

User defined commands must start with a capital letter.

Writing a plugin

Hopefully, at this point we know enough about VimL to be able to write our plugin.

The plugin that I’m going to write is going to be very simple. It will allow us to search for a string in multiple files in a directory (using grep). I used grep.vim as an example to learn how many things work, but my plugin is a lot simpler. I decided to name it Grepfrut

In Vim 8, plugins that will be loaded when Vim is started are located inside ~/.vim/pack/my-plugins/start. We will be creating a new folder called grepfrut, and inside the folder we’ll have this structure:

1
2
3
4
grepfrut/
|-- README.md
|-- plugin/
    |--- grepfrut.vim

All the code will be in grepfrut.vim. README.md will be used for documentation.

Before we start to write code, we have to decide what it will do. I want to keep it very simple. To use the plugin we will start with the Gf command:

1
:Gf <search string>

The user will be then prompted for the directory where they want to search (current directory will be prefilled):

1
Start searching from directory: /current/directory

Then we’ll allow the user to filter which files to search:

1
Search files matching pattern (Empty will match all):

And lastly, which files to not search:

1
Exclude files matching pattern (Empty will not exclude any file):

The results will be shown in a quickfix window and pressing enter on any of the results will open a tab with that file on the correct line.

We start by creating the command:

1
command! -nargs=1 Gf call s:Grepfrut(<f-args>)

The command receives only one argument (even if there are spaces, it will be read as a single argument) and that argument will be passed to a function called Grepfut. The s preceding the function name means that the funtion is local to the file (:help internal-variables to learn about scopes).

Let’s now define s:Grepfrut, which will basically take care of the UI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
" Entry point for the plugin
" search_string is the string we are going to grep for
function s:Grepfrut(search_string)
  " Ask user which directory they want to search
  let cwd = getcwd()
  let search_dir = input("Start searching from directory: ", cwd, "dir")

  " Which files to search
  let search_files = input("Search files matching pattern (Empty will match all): ")

  " Which files to not search
  let exclude_files = input("Exclude files matching pattern (Empty will not exclude any file): ")

  " Run the command
  echo "\r"
  let cmd = s:BuildGrepCommand(a:search_string, search_dir, search_files, exclude_files)
  call s:RunCommand(cmd)
endfunction

This function will basically show the correct prompts to the user and then leave the actual execution work to s:BuildGrepCommand and s:RunCommand.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
" Builds the command to grep for the search_string in all files
" search_string - The string we are searching for
" dir - The directory where the search will start
" include_files - Only files in `dir` matching this pattern will be grepped. If
"                 include_files is empty, all files will be grepped
" exclude_files - Files matching this pattern will not be grepped. If empty, all
"                 files will be grepped
function s:BuildGrepCommand(search_string, dir, include_files, exclude_files)
  let cmd = "find " . a:dir . " -type f "

  if a:include_files != ""
    let cmd = cmd . " | grep \"" . a:include_files . "\""
  endif

  if a:exclude_files != ""
    let cmd = cmd . " | grep -v \"" . a:exclude_files . "\""
  endif

  let cmd = cmd . " | xargs grep -n \"" . a:search_string . "\""

  return cmd
endfunction

This function builds the correct grep command based on the user input. For example, if the user is searching for something in /some/dir in all cpp files, except the ones with test in the name, it will generate this command:

1
find /some/dir -type f | grep "cpp" | grep -v "test" | xargs grep -n "something"

The last thing left is to execute the command and add it to the quickfix:

1
2
3
4
5
6
7
8
9
" Run the command and show results in quickfix
" cmd - The grep command that will be executed
function s:RunCommand(cmd)
  let cmd_output = system(a:cmd)

  " Open the output in a quickfix window
  cgetexpr cmd_output
  copen
endfunction

That’s it. A simple Vim plugin. I uploaded the Grepfrut plugin to Github in case you want to try it or see all the code together.

[ automation  programming  projects  vim  ]
Neovim as ESP32 IDE with Clangd LSP
Getting Started With Neovim
Monitoring Kubernetes Resources with Fabric8 Informers
Setting up LSP in Vim
Introduction to Google App Engine