├── LICENSE ├── README.md ├── autoload └── vim_erlang_tags.vim ├── bin ├── vim-erlang-tags.erl └── vim_erlang_tags.erl └── plugin └── vim-erlang-tags.vim /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Csaba Hoch 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-erlang-tags 2 | 3 | ## Table of Contents 4 | 5 | * [The idea](#the-idea) 6 | * [Installation](#installation) 7 | * [Usage](#usage) 8 | * [Generate tags](#generate-tags) 9 | * [Options](#options) 10 | * [`g:erlang_tags_ignore`](#gerlang_tags_ignore) 11 | * [`g:erlang_tags_auto_update`](#gerlang_tags_auto_update) 12 | * [`g:erlang_tags_auto_update_current`](#gerlang_tags_auto_update_current) 13 | * [`g:erlang_tags_outfile`](#gerlang_tags_outfile) 14 | * [`g:erlang_tags_follow`](#gerlang_tags_follow) 15 | * [`g:erlang_tags_otp`](#gerlang_tags_otp) 16 | * [Automating generating tags](#automating-generating-tags) 17 | * [Make Vim use the tags](#make-vim-use-the-tags) 18 | * [Using the Vim tag search commands](#using-the-vim-tag-search-commands) 19 | * [Contributing](#contributing) 20 | 21 | ## The idea 22 | 23 | `vim-erlang-tags` **creates a tags file** (from Erlang source files), which can 24 | be used by Vim. 25 | 26 | When using Exuberant ctags or etags, the generated tags will contain function 27 | names, but there will be no `module:function` tags. This is a problem 28 | because if several functions (in different modules) have the same name, the 29 | text editor will not know which one to jump to. 30 | 31 | The idea of this script is to generate `module:function` tags too. This way the 32 | code will be **easier to navigate than with ctags or etags**. The original idea 33 | is from László Lövei. 34 | 35 | Since `:` is not a keyword character when editing Erlang files in Vim, this 36 | repository also contains a Vim plugin, which modifies the following normal mode 37 | commands to add `:` to the `iskeyword` option for Erlang files while they are 38 | jumping to the location of the tag that is under the cursor: 39 | 40 | ``` 41 | CTRL-] 42 | g 43 | 44 | g] 45 | g CTRL-] 46 | ``` 47 | 48 | ## Installation 49 | 50 |
51 | Vim's built-in package manager 52 | 53 | This is the recommended installation method if you use at least Vim 8 and you 54 | don't use another package manager. 55 | 56 | Information about Vim's built-in package manager: [`:help packages`]. 57 | 58 | Installation steps: 59 | 60 | 1. Clone this repository (you can replace `foo` with the directory name of your 61 | choice): 62 | 63 | ```sh 64 | $ git clone https://github.com/vim-erlang/vim-erlang-tags.git \ 65 | ~/.vim/pack/foo/start/vim-erlang-tags 66 | ``` 67 | 68 | 2. Restart Vim. 69 |
70 | 71 |
72 | Pathogen 73 | 74 | Information about Pathogen: [Pathogen repository]. 75 | 76 | Installation steps: 77 | 78 | 1. Clone this repository: 79 | 80 | ``` 81 | $ git clone https://github.com/vim-erlang/vim-erlang-tags.git \ 82 | ~/.vim/bundle/vim-erlang-tags 83 | ``` 84 | 85 | 2. Restart Vim. 86 |
87 | 88 |
89 | Vundle 90 | 91 | Information about Vundle: [Vundle repository]. 92 | 93 | Installation steps: 94 | 95 | 1. Add `vim-erlang-tags` to your plugin list in `.vimrc` by inserting 96 | the line that starts with `Plugin`: 97 | 98 | ``` 99 | call vundle#begin() 100 | [...] 101 | Plugin 'vim-erlang/vim-erlang-tags' 102 | [...] 103 | call vundle#end() 104 | ``` 105 | 106 | 2. Restart Vim. 107 | 108 | 3. Run `:PluginInstall`. 109 |
110 | 111 |
112 | Vim-Plug 113 | 114 | Information about Vim-Plug: [vim-plug repository]. 115 | 116 | Installation steps: 117 | 118 | 1. Add `vim-erlang-tags` to your plugin list in `.vimrc` by inserting the 119 | line that starts with `Plug`: 120 | 121 | ``` 122 | call plug#begin() 123 | [...] 124 | Plug 'vim-erlang/vim-erlang-tags' 125 | [...] 126 | call plug#end() 127 | ``` 128 | 129 | 2. Restart Vim. 130 | 131 | 3. Run `:PlugInstall`. 132 |
133 | 134 | ## Usage 135 | 136 | Let's say you would like to use tags for your Erlang project. 137 | 138 | ### Generate tags 139 | 140 | First you need to generate the tags. 141 | 142 | You can do that either in the command line: 143 | 144 | ``` 145 | $ cd /path/to/my_erlang_project 146 | $ /path/to/vim-erlang-tags/bin/vim_erlang_tags.erl 147 | ``` 148 | 149 | Or within Vim by executing the following command: 150 | 151 | ``` 152 | :ErlangTags 153 | ``` 154 | 155 | Note that for the latter command, the current working directory will be used 156 | (`:help pwd` to find out more). 157 | 158 | ### Options 159 | 160 | #### `g:erlang_tags_ignore` 161 | 162 | Add ignore path for tags generation. Use a string or list of strings like: 163 | 164 | ``` 165 | let g:erlang_tags_ignore = 'rel' 166 | let g:erlang_tags_ignore = ['rel'] 167 | ``` 168 | 169 | Default: doesn't exist. 170 | 171 | #### `g:erlang_tags_auto_update` 172 | 173 | If exists and set to 1, this plugin will be triggered when an Erlang buffer is 174 | written. Warning: this may cost lots of CPU if you have a large project. Note 175 | that it might not work on Windows. 176 | 177 | Default: doesn't exist. 178 | 179 | #### `g:erlang_tags_auto_update_current` 180 | 181 | If exists and set to 1, this plugin will be triggered when an Erlang buffer is 182 | written. In this case, it will attempt to update only the currently modified 183 | file. As a limitation, this will consider that your `tags` file is on the 184 | current directory, as returned by vim's `getcwd()`. 185 | Note that it might not work on Windows. 186 | 187 | Default: doesn't exist. 188 | 189 | #### `g:erlang_tags_outfile` 190 | 191 | This option specifies the name of the generated tags file. By default, the 192 | output file will be `./tags`. 193 | 194 | #### `g:erlang_tags_follow` 195 | 196 | If exists and set to 1, this plugin will follow symbolic links. 197 | 198 | Default: doesn't follow symbolic links. 199 | 200 | #### `g:erlang_tags_otp` 201 | 202 | If exists and set to 1, this plugin will include the currently used OTP lib_dir. 203 | 204 | Default: doesn't include OTP lib_dir. 205 | 206 | ### Automating generating tags 207 | 208 | To keep the tags file up-to-date you can re-run these commands periodically, or 209 | automate the process by creating a commit/checkout hook or a crontab entry. 210 | 211 | If you use Git, creating a checkout hook is simple: 212 | 213 | ``` 214 | echo '#!/bin/bash' > .git/hooks/post-checkout 215 | echo '/path/to/vim-erlang-tags/bin/vim_erlang_tags.erl' > .git/hooks/post-checkout 216 | chmod +x .git/hooks/post-checkout 217 | cp -i .git/hooks/post-checkout .git/hooks/post-commit 218 | ``` 219 | 220 | ### Make Vim use the tags 221 | 222 | Add the following line to your `.vimrc`: 223 | 224 | ``` 225 | :set tags^=/path/to/my_erlang_project/tags 226 | ``` 227 | 228 | This will explicitly add the `tags` file to the list of known tags locations. 229 | 230 | Reopen Vim or just execute `:source $MYVIMRC` – now all your function names, 231 | records, macros and file names are available with the Vim tag search commands. 232 | 233 | ### Using the Vim tag search commands 234 | 235 | The few most useful tag search commands are the following: 236 | 237 | - `CTRL-]`: jump to the definition of the function/record/macro under the cursor 238 | - `:tj ident`: jump to the definition of `ident` (function/record/macro name) 239 | 240 | For more information on those commands, see `:help tagsrch.txt`. 241 | 242 | ## Contributing 243 | 244 | * Please read the [Contributing][vim-erlang-contributing] section of the 245 | vim-erlang README. 246 | 247 | * If you modify `vim_erlang_tags.erl`, please update the tests in in the 248 | vim-erlang repository. 249 | 250 | 251 | 252 | [`:help packages`]: https://vimhelp.org/repeat.txt.html#packages 253 | [Pathogen repository]: https://github.com/tpope/vim-pathogen 254 | [vim-erlang-contributing]: https://github.com/vim-erlang/vim-erlang#contributing 255 | [vim-plug repository]: https://github.com/junegunn/vim-plug 256 | [Vundle repository]: https://github.com/VundleVim/Vundle.vim 257 | -------------------------------------------------------------------------------- /autoload/vim_erlang_tags.vim: -------------------------------------------------------------------------------- 1 | let s:exec_script = expand(':p:h') . "/../bin/vim_erlang_tags.erl" 2 | 3 | function! s:GetExecuteCmd() 4 | let script_opts = "" 5 | 6 | if exists("g:erlang_tags_ignore") 7 | let ignored_paths = (type(g:erlang_tags_ignore) == type("string") ? 8 | \ [ g:erlang_tags_ignore ] : 9 | \ g:erlang_tags_ignore) 10 | 11 | for path in ignored_paths 12 | let script_opts = script_opts . " --ignore " . path 13 | endfor 14 | endif 15 | 16 | if exists("g:erlang_tags_outfile") && g:erlang_tags_outfile != "" 17 | let script_opts = script_opts . " --output " . g:erlang_tags_outfile 18 | endif 19 | 20 | if exists("g:erlang_tags_follow") && g:erlang_tags_follow == 1 21 | let script_opts = script_opts . " --follow" 22 | endif 23 | 24 | if exists("g:erlang_tags_otp") && g:erlang_tags_otp == 1 25 | let script_opts = script_opts . " --otp" 26 | endif 27 | 28 | return s:exec_script . script_opts 29 | endfunction 30 | 31 | function! vim_erlang_tags#VimErlangTags(...) 32 | let param = join(a:000, " ") 33 | let exec_cmd = s:GetExecuteCmd() 34 | let script_output = system(exec_cmd . " " . param) 35 | if !v:shell_error 36 | return 0 37 | else 38 | echoerr "vim-erlang-tag failed with: " . script_output 39 | endif 40 | endfunction 41 | 42 | function! vim_erlang_tags#AsyncVimErlangTags(...) 43 | let param = join(a:000, " ") 44 | let exec_cmd = s:GetExecuteCmd() 45 | call system(exec_cmd . " " . param . " " . '&') 46 | endfunction 47 | 48 | function! vim_erlang_tags#VimErlangTagsSelect(split) 49 | if a:split 50 | split 51 | endif 52 | let curr_line = getline('.') 53 | if curr_line[col('.') - 1] =~# '[#?]' 54 | normal! w 55 | endif 56 | let orig_isk = &isk 57 | set isk+=: 58 | normal! "_viwo 59 | if curr_line[col('.') - 2] =~# '[#?]' 60 | normal! h 61 | endif 62 | let &isk = orig_isk 63 | let module_marco_start = stridx(curr_line, "?MODULE", col('.') - 1) 64 | if module_marco_start == col('.') - 1 65 | " The selected text starts with ?MODULE, so re-select only the 66 | " function name. 67 | normal! ov"_viwo 68 | endif 69 | endfunction 70 | 71 | if !exists("s:os") 72 | if has("win64") || has("win32") || has("win16") 73 | let s:os = "Windows" 74 | else 75 | let s:os = substitute(system('uname'), '\n', '', '') 76 | endif 77 | endif 78 | 79 | " https://vim.fandom.com/wiki/Autocmd_to_update_ctags_file 80 | function! vim_erlang_tags#DelTagOfFile(file) 81 | let fullpath = a:file 82 | let cwd = getcwd() 83 | let tagfilename = cwd . "/tags" 84 | let f = substitute(fullpath, cwd . "/", "", "") 85 | let f = escape(f, './') 86 | let cmd = "" 87 | if s:os == "Darwin" 88 | let cmd = 'sed -i "" "/' . f . '/d" "' . tagfilename . '"' 89 | elseif s:os == "Linux" 90 | let cmd = 'sed -i "/' . f . '/d" "' . tagfilename . '"' 91 | endif 92 | return system(cmd) 93 | endfunction 94 | 95 | function! vim_erlang_tags#UpdateTags() 96 | let f0 = expand("%:p") 97 | let cwd = getcwd() 98 | let f = substitute(f0, cwd . "/", "", "") 99 | let tagfilename = cwd . "/tags" 100 | let temptags = cwd . "/temptags" 101 | let exec_cmd = s:GetExecuteCmd() 102 | call vim_erlang_tags#DelTagOfFile(f) 103 | let param = " --include " . f . " --output " . temptags 104 | call vim_erlang_tags#VimErlangTags(param) 105 | let cmd = "tail -n +2 " . temptags . " | sort -o " . tagfilename . " -m -u " . tagfilename . " - " 106 | let resp = system(cmd) 107 | endfunction 108 | -------------------------------------------------------------------------------- /bin/vim-erlang-tags.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | -mode(compile). 3 | main(_Args) -> 4 | Help = 5 | "The vim-erlang-tags.erl script has been moved to vim_erlang_tags.erl. 6 | Please use that script instead." 7 | , io:format("~s", [Help]). 8 | -------------------------------------------------------------------------------- /bin/vim_erlang_tags.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- 3 | %% ex: ts=4 sw=4 ft=erlang et 4 | 5 | %%% This script creates a tags file that can be used by Vim. 6 | %%% 7 | %%% See more information in the {@link print_help/0} function. 8 | 9 | %%% Copyright 2013-2020 Csaba Hoch 10 | %%% Copyright 2013 Adam Rutkowski 11 | %%% 12 | %%% Licensed under the Apache License, Version 2.0 (the "License"); 13 | %%% you may not use this file except in compliance with the License. 14 | %%% You may obtain a copy of the License at 15 | %%% 16 | %%% http://www.apache.org/licenses/LICENSE-2.0 17 | %%% 18 | %%% Unless required by applicable law or agreed to in writing, software 19 | %%% distributed under the License is distributed on an "AS IS" BASIS, 20 | %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | %%% See the License for the specific language governing permissions and 22 | %%% limitations under the License. 23 | 24 | %%% Recommended reading: 25 | %%% 26 | %%% - http://ctags.sourceforge.net/FORMAT 27 | %%% - http://vimdoc.sourceforge.net/htmldoc/tagsrch.html#tags-file-format 28 | 29 | %%% The EtsTags ets table has the following scheme: 30 | %%% 31 | %%% {{TagName, FilePath, Scope, Kind}, TagAddress} 32 | %%% 33 | %%% TagName :: binary(), 34 | %%% FilePath :: binary(), 35 | %%% Scope :: scope() 36 | %%% Kind :: char() 37 | %%% TagAddress :: tag_address_binary() 38 | %%% 39 | %%% Or in more readable notation: 40 | %%% 41 | %%% {TagName, FilePath, Scope, Kind} -> TagAddress 42 | %%% 43 | %%% Examples of entries (and the tags output generated from them): 44 | %%% 45 | %%% {ErlFileName, FilePath, global, $F} -> TagAddress 46 | %%% myfile.erl ./myfile.erl 1;" F 47 | %%% 48 | %%% {HrlFileName, FilePath, global, $F} -> TagAddress 49 | %%% myfile.hrl ./myfile.hrl 1;" F 50 | %%% 51 | %%% {ModName, FilePath, global, $M} -> TagAddress 52 | %%% myfile ./myfile.erl 1;" M 53 | %%% 54 | %%% {FuncName, FilePath, local, $f} -> TagAddress 55 | %%% f ./mymod.erl /^f\>/;" f file: 56 | %%% 57 | %%% {FuncName, FilePath, global, $f} -> TagAddress 58 | %%% mymod:f ./mymod.erl /^f\>/;" f 59 | %%% 60 | %%% {Type, FilePath, local, $t} -> TagAddress 61 | %%% mytype ./mymod.erl /^-type\s\*\/;" t file: 62 | %%% 63 | %%% {Type, FilePath, global, $t} -> TagAddress 64 | %%% mymod:mytype ./mymod.erl /^-type\s\*\/;" t 65 | %%% 66 | %%% {Record, FilePath, local, $r} -> TagAddress 67 | %%% myrec ./mymod.erl /^-record\s\*\/;" r file: 68 | %%% 69 | %%% {Record, FilePath, global, $r} -> TagAddress 70 | %%% myrec ./myhrl.hrl /^-record\s\*\/;" r 71 | %%% 72 | %%% {Macro, FilePath, local, $d} -> TagAddress 73 | %%% mymac ./mymod.erl /^-define\s\*\/;" d file: 74 | %%% 75 | %%% {Macro, FilePath, global, $d} -> TagAddress 76 | %%% mymac ./myhrl.hrl /^-define\s\*\/;" d 77 | 78 | % 'compile' mode gives better error messages if the script throws an error. 79 | -mode(compile). 80 | 81 | -include_lib("kernel/include/file.hrl"). 82 | 83 | %%%============================================================================= 84 | %%% Macros 85 | %%%============================================================================= 86 | 87 | -define(COMPILE, fun(Re) -> 88 | {ok, CRE} = re:compile(Re, [multiline]), 89 | CRE 90 | end). 91 | 92 | -define(RE_FUNCTIONS, 93 | ?COMPILE("^([a-z][a-zA-Z0-9_@]*)\\s*\\(")). 94 | 95 | -define(RE_TYPESPECS1, 96 | ?COMPILE("^-\\s*(type|opaque)\\s*([a-zA-Z0-9_@]+)\\b")). 97 | 98 | -define(RE_TYPESPECS2, 99 | ?COMPILE("^-\\s*(type|opaque)\\s*'([^ \\t']+)'")). 100 | 101 | -define(RE_DEFINES1, 102 | ?COMPILE("^-\\s*(record|define)\\s*\\(?\\s*([a-zA-Z0-9_@]+)\\b")). 103 | 104 | -define(RE_DEFINES2, 105 | ?COMPILE("^-\\s*(record|define)\\s*\\(?\\s*'([^ \\t']+)'")). 106 | 107 | -define(RE_BUILD, 108 | ?COMPILE("\\b_build\\b")). 109 | 110 | -define(DEFAULT_PATH, "."). 111 | 112 | %%%============================================================================= 113 | %%% Types 114 | %%%============================================================================= 115 | 116 | -record(parsed_params, { 117 | include = [] :: [string()], 118 | ignore = [] :: [string()], 119 | output = [] :: [string()], 120 | follow = false :: boolean(), 121 | otp = false :: boolean(), 122 | verbose = false :: boolean(), 123 | help = false :: boolean() 124 | }). 125 | 126 | -type parsed_params() :: #parsed_params{}. 127 | %% The parameters of the script after they are parsed. 128 | 129 | -record(config, { 130 | explore :: [file:filename()], 131 | output :: file:filename(), 132 | help :: boolean() 133 | }). 134 | 135 | -type config() :: #config{}. 136 | %% Configuration that describes what the script needs to do. 137 | %% 138 | %% {@link clean_opts/1} 139 | 140 | -type cmd_line_arg() :: string(). 141 | %% A command line argument (before parsing). 142 | 143 | -type cmd_line_arguments() :: [cmd_line_arg()]. 144 | %% A list of command line argument (before parsing). 145 | 146 | -type cmd_param() :: include | ignore | output | follow | otp | verbose | help. 147 | %% A command line parameter (after parsing). 148 | 149 | -type command_type() :: boolean | stateful. 150 | %% The type of a command line parameter. 151 | 152 | -type param_value() :: boolean() | % if command_type() is boolean 153 | [cmd_line_arguments()]. % if command_type() is stateful 154 | %% The value of a command line parameter. 155 | 156 | -type tag_address_iolist() :: iolist(). 157 | %% Same as {@link tag_address_iolist()} but as an iolist. 158 | 159 | -type tag_address_binary() :: binary(). 160 | %% The Ex command that positions the cursor on the tag. 161 | %% 162 | %% See `:help tags-file-format' and search for `{tagaddress}'. 163 | %% 164 | %% Example: `/^myfunction\>/'. 165 | 166 | -type scope() :: global | local. 167 | %% Shows the scope of a tag. If a tag is local, then the editor should jump to 168 | %% it only from the same file. If a tag is global, then the editor should jump 169 | %% to it from any other file. 170 | %% 171 | %% See `:help tags-file-format' and search for `"file:"'. 172 | 173 | %%%============================================================================= 174 | %%% Main function 175 | %%%============================================================================= 176 | 177 | %%------------------------------------------------------------------------------ 178 | %% @doc This function is the entry point of the script. 179 | %% @end 180 | %%------------------------------------------------------------------------------ 181 | -spec main(Args) -> ok when 182 | Args :: [string()]. 183 | main(Args) -> 184 | log("Entering main. Args are ~p~n~n", [Args]), 185 | ParsedArgs = parse_args(#parsed_params{}, Args), 186 | set_verbose_flag(ParsedArgs), 187 | Opts = clean_opts(ParsedArgs), 188 | run(Opts). 189 | 190 | %%------------------------------------------------------------------------------ 191 | %% @doc Read the files and generate the tags. 192 | %% @end 193 | %%------------------------------------------------------------------------------ 194 | -spec run(Config) -> ok when 195 | Config :: config(). 196 | run(#config{help = true}) -> 197 | print_help(); 198 | run(#config{explore = Explore, output = TagFile}) -> 199 | EtsTags = create_tags(Explore), 200 | ok = tags_to_file(EtsTags, TagFile), 201 | ets:delete(EtsTags), 202 | ok. 203 | 204 | %%%============================================================================= 205 | %%% Parse command line arguments 206 | %%%============================================================================= 207 | 208 | %%------------------------------------------------------------------------------ 209 | %% @doc Parse the the command line arguments. 210 | %% @end 211 | %%------------------------------------------------------------------------------ 212 | -spec parse_args(Acc, CmdLineArgs) -> Result when 213 | Acc :: parsed_params(), 214 | CmdLineArgs :: cmd_line_arguments(), 215 | Result :: parsed_params(). 216 | parse_args(Opts, []) -> 217 | Opts; 218 | parse_args(Opts, AllCliArgs) -> 219 | {Param, RestCliArgs1} = parse_next_arg(AllCliArgs), 220 | {ParamValue, RestCliArgs2} = 221 | case get_command_type(Param) of 222 | boolean -> 223 | {true, RestCliArgs1}; 224 | stateful -> 225 | get_full_arg_state( 226 | Param, param_get(Param, Opts), RestCliArgs1) 227 | end, 228 | parse_args(param_set(Param, ParamValue, Opts), RestCliArgs2). 229 | 230 | %%------------------------------------------------------------------------------ 231 | %% @doc Parse the next command line argument. 232 | %% @end 233 | %%------------------------------------------------------------------------------ 234 | -spec parse_next_arg(AllCliArgs) -> Result when 235 | AllCliArgs :: nonempty_list(cmd_line_arg()), 236 | Result :: {cmd_param(), 237 | Rest :: cmd_line_arguments()}. 238 | parse_next_arg([Arg | RestArgs] = AllCliArgs) -> 239 | lists:foldl( 240 | fun({Param, ParamList}, Acc) -> 241 | case lists:member(Arg, ParamList) of 242 | true -> {Param, RestArgs}; 243 | _ -> Acc 244 | end 245 | end, %% If the parameter is not recognised, just throw it into include 246 | {include, AllCliArgs}, 247 | allowed_cmd_params()). 248 | 249 | %%------------------------------------------------------------------------------ 250 | %% @doc Return the type of a command. 251 | %% @end 252 | %%------------------------------------------------------------------------------ 253 | -spec get_command_type(Cmd) -> Result when 254 | Cmd :: cmd_param(), 255 | Result :: command_type(). 256 | get_command_type(C) when C =:= include; 257 | C =:= ignore; 258 | C =:= output -> 259 | stateful; 260 | get_command_type(B) when B =:= follow; 261 | B =:= otp; 262 | B =:= verbose; 263 | B =:= help -> 264 | boolean. 265 | 266 | %%------------------------------------------------------------------------------ 267 | %% @doc Return the list of allowed command line parameters. 268 | %% @end 269 | %%------------------------------------------------------------------------------ 270 | -spec allowed_cmd_params() -> Result when 271 | Result :: [{cmd_param(), cmd_line_arguments()}]. 272 | allowed_cmd_params() -> 273 | [ 274 | {include, ["-i", "--include", "--"]}, 275 | {ignore, ["-g", "--ignore"]}, 276 | {output, ["-o", "--output"]}, 277 | {follow, ["--follow"]}, 278 | {otp, ["-p", "--otp"]}, 279 | {verbose, ["-v", "--verbose"]}, 280 | {help, ["-h", "--help"]} 281 | ]. 282 | 283 | %%------------------------------------------------------------------------------ 284 | %% @doc Return arguments of the current parameter. 285 | %% @end 286 | %%------------------------------------------------------------------------------ 287 | -spec get_full_arg_state(Param, CurrentParamValue, RestCliArgs) -> Result when 288 | Param :: cmd_param(), 289 | CurrentParamValue :: cmd_line_arguments(), 290 | RestCliArgs :: cmd_line_arguments(), 291 | Result :: {NewParamState :: cmd_line_arguments(), 292 | RestCliArgs :: cmd_line_arguments()}. 293 | get_full_arg_state(Param, CurrentParamValue, RestCliArgs) -> 294 | log("Parsing args for parameter ~p~n", [Param]), 295 | {NewArgs, Rest} = consume_until_new_command(RestCliArgs), 296 | case NewArgs of 297 | [] -> log_error("Arguments needed for ~s.~n", [Param]); 298 | _ -> ok 299 | end, 300 | {NewArgs ++ CurrentParamValue, Rest}. 301 | 302 | %%------------------------------------------------------------------------------ 303 | %% @doc Consume the arguments until there is a new parameter. 304 | %% @end 305 | %%------------------------------------------------------------------------------ 306 | -spec consume_until_new_command(Args) -> {ConsumedArgs, RestArgs} when 307 | Args :: cmd_line_arguments(), 308 | ConsumedArgs :: cmd_line_arguments(), 309 | RestArgs :: cmd_line_arguments(). 310 | consume_until_new_command(Args) -> 311 | log(" Consuming args ~p~n", [Args]), 312 | States = lists:foldl( 313 | fun({_,S}, Acc) -> S ++ Acc end, [], allowed_cmd_params()), 314 | lists:splitwith( 315 | fun("-" ++ _ = El) -> 316 | case lists:member(El, States) of 317 | true -> false; 318 | _ -> log_error("Unknown argument: ~s~n", [El]), halt(1) 319 | end; 320 | (_El) -> 321 | true 322 | end, Args). 323 | 324 | %%------------------------------------------------------------------------------ 325 | %% @doc Get a parameter from the "parsed parameters" record. 326 | %% @end 327 | %%------------------------------------------------------------------------------ 328 | -spec param_get(Parameter, ParsedParams) -> Result when 329 | Parameter :: cmd_param(), 330 | ParsedParams :: parsed_params(), 331 | Result :: param_value(). 332 | param_get(include, #parsed_params{include = Include}) -> Include; 333 | param_get(ignore, #parsed_params{ignore = Ignore}) -> Ignore; 334 | param_get(output, #parsed_params{output = Output}) -> Output; 335 | param_get(otp, #parsed_params{otp = Otp}) -> Otp; 336 | param_get(verbose, #parsed_params{verbose = Verbose}) -> Verbose; 337 | param_get(help, #parsed_params{help = Help}) -> Help. 338 | 339 | %%------------------------------------------------------------------------------ 340 | %% @doc Set a parameter in the "parsed parameters" record. 341 | %% @end 342 | %%------------------------------------------------------------------------------ 343 | -spec param_set(Parameter, Value, ParsedParams) -> ParsedParams when 344 | Parameter :: cmd_param(), 345 | Value :: param_value(), 346 | ParsedParams :: parsed_params(). 347 | param_set(include, Value, PP) -> PP#parsed_params{include = Value}; 348 | param_set(ignore, Value, PP) -> PP#parsed_params{ignore = Value}; 349 | param_set(output, Value, PP) -> PP#parsed_params{output = Value}; 350 | param_set(follow, Value, PP) -> PP#parsed_params{follow = Value}; 351 | param_set(otp, Value, PP) -> PP#parsed_params{otp = Value}; 352 | param_set(verbose, Value, PP) -> PP#parsed_params{verbose = Value}; 353 | param_set(help, Value, PP) -> PP#parsed_params{help = Value}. 354 | 355 | %%%============================================================================= 356 | %%% Apply command line parameters 357 | %%%============================================================================= 358 | 359 | %%------------------------------------------------------------------------------ 360 | %% @doc Set "verbose" mode according to the CLI parameters. 361 | %% @end 362 | %%------------------------------------------------------------------------------ 363 | -spec set_verbose_flag(ParsedParams) -> ok when 364 | ParsedParams :: parsed_params(). 365 | set_verbose_flag(#parsed_params{verbose = Verbose}) -> 366 | put(verbose, Verbose), 367 | log("Verbose mode on.~n"). 368 | 369 | %%------------------------------------------------------------------------------ 370 | %% @doc Convert the "parsed parameters" record into the "config" record. 371 | %% @end 372 | %%------------------------------------------------------------------------------ 373 | -spec clean_opts(ParsedParams) -> Result when 374 | ParsedParams :: parsed_params(), 375 | Result :: config(). 376 | clean_opts(#parsed_params{help = true}) -> 377 | #config{help = true}; 378 | clean_opts(#parsed_params{include = []} = Opts0) -> 379 | log("Set includes to default current dir.~n"), 380 | clean_opts(Opts0#parsed_params{include = [?DEFAULT_PATH]}); 381 | clean_opts(#parsed_params{otp = true, include = Inc} = Opts0) -> 382 | log("Including OTP in.~n"), 383 | AllIncludes = [code:lib_dir() | Inc], 384 | clean_opts(Opts0#parsed_params{include = AllIncludes, otp = false}); 385 | clean_opts(#parsed_params{output = []} = Opts0) -> 386 | log("Set output to default 'tags'.~n"), 387 | clean_opts(Opts0#parsed_params{output = ["tags"]}); 388 | clean_opts(#parsed_params{include = Included, 389 | ignore = Ignored, 390 | output = [Output], 391 | follow = FollowSymLinks}) -> 392 | log("Set includes to default current dir.~n"), 393 | #config{explore = to_explore_as_include_minus_ignored( 394 | Included, Ignored, FollowSymLinks), 395 | output = Output}. 396 | 397 | %%------------------------------------------------------------------------------ 398 | %% @doc Expand all the paths given in included and in ignored to actual 399 | %% filenames, and then subtracts the excluded ones from the included. 400 | %% @end 401 | %%------------------------------------------------------------------------------ 402 | -spec to_explore_as_include_minus_ignored(Included, Ignored, 403 | FollowSymLinks) -> Result when 404 | Included :: [string()], 405 | Ignored :: [string()], 406 | FollowSymLinks :: boolean(), 407 | Result :: [file:filename()]. 408 | to_explore_as_include_minus_ignored(Included, Ignored, FollowSymLinks) -> 409 | AllIncluded = lists:append(expand_dirs(Included, FollowSymLinks)), 410 | AllIgnored = lists:append(expand_dirs(Ignored, FollowSymLinks)), 411 | lists:subtract(AllIncluded, AllIgnored). 412 | 413 | %%------------------------------------------------------------------------------ 414 | %% @doc Return all Erlang source files under the directories (recursively). 415 | %% 416 | %% The regular files are simply returned. 417 | %% @end 418 | %%------------------------------------------------------------------------------ 419 | -spec expand_dirs(DirOrFileNames, FollowSymLinks) -> Result when 420 | DirOrFileNames :: [string()], 421 | FollowSymLinks :: boolean(), 422 | Result :: [file:filename()]. 423 | expand_dirs(DirOrFilenames, FollowSymLinks) -> 424 | lists:map(fun(DirOrFilename) -> 425 | expand_dirs_or_filenames(DirOrFilename, FollowSymLinks) 426 | end, DirOrFilenames). 427 | 428 | %%------------------------------------------------------------------------------ 429 | %% @doc Return all Erlang source files under a directory (recursively). 430 | %% 431 | %% If a file is given, return that file. 432 | %% @end 433 | %%------------------------------------------------------------------------------ 434 | -spec expand_dirs_or_filenames(DirOrFileName, FollowSymLinks) -> Result when 435 | DirOrFileName :: string(), 436 | FollowSymLinks :: boolean(), 437 | Result :: [file:filename()]. 438 | expand_dirs_or_filenames(DirOrFileName, FollowSymLinks) -> 439 | case {filelib:is_regular(DirOrFileName), 440 | filelib:is_dir(DirOrFileName)} of 441 | {true, false} -> 442 | % It's a file -> return the file 443 | [DirOrFileName]; 444 | {false, true} when FollowSymLinks -> 445 | % It's a directory -> return all source files inside the directory. 446 | % 447 | % Using '**' has an advantage over a simple recursive function that 448 | % it limits directory depths, which ensures that we eventually 449 | % terminate even if these are symlink loops. 450 | filelib:wildcard(DirOrFileName ++ "/**/*.{erl,hrl}"); 451 | {false, true} when not FollowSymLinks -> 452 | % It's a directory -> return all source files inside the directory 453 | find_source_files(DirOrFileName); 454 | {false, false} -> 455 | case filelib:wildcard(DirOrFileName) of 456 | [] -> 457 | % It's neither a file, nor a directory, not a wildcard -> 458 | % error 459 | log_error("File \"~p\" is not a proper file.~n", [DirOrFileName]), 460 | []; 461 | [_|_] = Filenames -> 462 | % It's a wildcard -> expand it 463 | lists:append(expand_dirs(Filenames, FollowSymLinks)) 464 | end 465 | end. 466 | 467 | %%------------------------------------------------------------------------------ 468 | %% @doc Return all *.erl and *.hrl files in the given directory. 469 | %% 470 | %% Symbolic links are *not* followed. 471 | %% @end 472 | %%------------------------------------------------------------------------------ 473 | -spec find_source_files(Dir) -> Result when 474 | Dir :: file:name_all(), 475 | Result :: [file:filename()]. 476 | find_source_files(Dir) -> 477 | case file:list_dir(Dir) of 478 | {ok, FileNames} -> 479 | lists:append( 480 | [begin 481 | FilePath = 482 | case Dir of 483 | "." -> 484 | % Don't add the './' to the beginning. This way 485 | % we behave the same way when the "--follow" 486 | % option and thus filelib:wildcard/1 is used. 487 | FileName; 488 | _ -> 489 | filename:join(Dir, FileName) 490 | end, 491 | case get_file_type(FilePath) of 492 | {ok, directory} -> 493 | log("Directory found: ~ts~n", [FilePath]), 494 | find_source_files(FilePath); 495 | {ok, regular} -> 496 | case filename:extension(FilePath) of 497 | Ext when Ext == ".erl"; 498 | Ext == ".hrl" -> 499 | log("Source file found: ~ts~n", [FilePath]), 500 | case FilePath of 501 | "./" ++ FilePathRest -> 502 | [FilePathRest]; 503 | _ -> 504 | [FilePath] 505 | end; 506 | _ -> 507 | [] 508 | end; 509 | {ok, _} -> 510 | []; 511 | {error, Reason} -> 512 | log_error("Cannot find file or directory '~ts': ~p.~n", 513 | [FilePath, Reason]), 514 | [] 515 | end 516 | end || FileName <- lists:sort(FileNames)]); 517 | {error, Reason} -> 518 | log_error("Cannot read directory '~ts': ~p.~n", [Dir, Reason]), 519 | [] 520 | end. 521 | 522 | %%------------------------------------------------------------------------------ 523 | %% @doc Return the type of the given file. 524 | %% @end 525 | %%------------------------------------------------------------------------------ 526 | -spec get_file_type(FileName) -> Result when 527 | FileName :: file:name_all(), 528 | Type :: device | directory | other | regular | symlink, 529 | Result :: {ok, Type} | {error, any()}. 530 | get_file_type(FileName) -> 531 | case file:read_link_info(FileName) of 532 | {ok, #file_info{type = FileType}} -> 533 | {ok, FileType}; 534 | {error, Reason} -> 535 | {error, Reason} 536 | end. 537 | 538 | %%%============================================================================= 539 | %%% Create tags from directory trees and file lists 540 | %%%============================================================================= 541 | 542 | %%------------------------------------------------------------------------------ 543 | %% @doc Read the given Erlang source files and return an ets table that contains 544 | %% the appropriate tags. 545 | %% @end 546 | %%------------------------------------------------------------------------------ 547 | -spec create_tags(Explore) -> Result when 548 | Explore :: [file:filename()], 549 | Result :: ets:tid(). 550 | create_tags(Explore) -> 551 | log("In create_tags, To explore: ~tp~n", [Explore]), 552 | EtsTags = ets:new(tags, 553 | [set, 554 | public, 555 | {write_concurrency,true}, 556 | {read_concurrency,false} 557 | ]), 558 | log("EtsTags table created.~n"), 559 | log("Starting processing of files~n"), 560 | Processes = process_filenames(Explore, EtsTags, []), 561 | lists:foreach( 562 | fun({Pid, Ref}) -> 563 | receive 564 | {'DOWN', Ref, process, Pid, normal} -> ok 565 | after 566 | 30000 -> 567 | log_error( 568 | "Error: scanning the source files took too long.~n", 569 | []), 570 | halt(1) 571 | end 572 | end, 573 | Processes), 574 | EtsTags. 575 | 576 | 577 | %%------------------------------------------------------------------------------ 578 | %% @doc Go through the given files: scan the Erlang files for tags. 579 | %% 580 | %% Here we now for sure that `Files` are indeed files with extensions *.erl or 581 | %% *.hrl. 582 | %% @end 583 | %%------------------------------------------------------------------------------ 584 | -spec process_filenames(Files, EtsTags, Processes) -> RetProcesses when 585 | Files :: [file:filename()], 586 | EtsTags :: ets:tid(), 587 | Processes :: [{pid(), reference()}], 588 | RetProcesses :: [{pid(), reference()}]. 589 | process_filenames([], _Tags, Processes) -> 590 | Processes; 591 | process_filenames([File|OtherFiles], EtsTags, Processes) -> 592 | Verbose = get(verbose), 593 | P = spawn_monitor(fun() -> add_tags_from_file(File, EtsTags, Verbose) end), 594 | process_filenames(OtherFiles, EtsTags, [P | Processes]). 595 | 596 | %%%============================================================================= 597 | %%% Scan a file or line for tags 598 | %%%============================================================================= 599 | 600 | %%------------------------------------------------------------------------------ 601 | %% @doc Read the given Erlang source file and add the appropriate tags to the 602 | %% EtsTags ets table. 603 | %% @end 604 | %%------------------------------------------------------------------------------ 605 | -spec add_tags_from_file(File, EtsTags, Verbose) -> ok when 606 | File :: file:filename(), 607 | EtsTags :: ets:tid(), 608 | Verbose :: boolean(). 609 | add_tags_from_file(File, EtsTags, Verbose) -> 610 | put(verbose, Verbose), 611 | log("~nProcessing file: ~ts~n", [File]), 612 | 613 | BaseName = filename:basename(File), % e.g. "mymod.erl" 614 | ModName = filename:rootname(BaseName), % e.g. "mymod" 615 | add_file_tag(EtsTags, File, BaseName, ModName), 616 | 617 | case file:read_file(File) of 618 | {ok, Contents} -> 619 | ok = scan_tags(Contents, {EtsTags, File, ModName}); 620 | Err -> 621 | log_error("File ~ts not readable: ~p~n", [File, Err]) 622 | end. 623 | 624 | %%------------------------------------------------------------------------------ 625 | %% @doc Add all tags found in the file to the ETS table. 626 | %% @end 627 | %%------------------------------------------------------------------------------ 628 | -spec scan_tags(Contents, {EtsTags, File, ModName}) -> ok when 629 | Contents :: binary(), 630 | EtsTags :: ets:tid(), 631 | File :: file:filename(), 632 | ModName :: string(). 633 | scan_tags(Contents, {EtsTags, File, ModName}) -> 634 | scan_tags_core( 635 | Contents, ?RE_FUNCTIONS, 636 | fun([_, FuncName]) -> 637 | add_func_tags(EtsTags, File, ModName, FuncName) 638 | end), 639 | scan_tags_core( 640 | Contents, ?RE_TYPESPECS1, 641 | fun([_, Attr, TypeName]) -> 642 | InnerPattern = [TypeName, "\\>"], 643 | add_type_tags(EtsTags, File, ModName, Attr, TypeName, InnerPattern) 644 | end), 645 | scan_tags_core( 646 | Contents, ?RE_TYPESPECS2, 647 | fun([_, Attr, TypeName]) -> 648 | InnerPattern = [$', TypeName, $'], 649 | add_type_tags(EtsTags, File, ModName, Attr, TypeName, InnerPattern) 650 | end), 651 | scan_tags_core( 652 | Contents, ?RE_DEFINES1, 653 | fun([_, Attr, Name]) -> 654 | InnerPattern = [Name, "\\>"], 655 | add_record_or_macro_tag(EtsTags, File, Attr, Name, InnerPattern) 656 | end), 657 | scan_tags_core( 658 | Contents, ?RE_DEFINES2, 659 | fun([_, Attr, Name]) -> 660 | InnerPattern = [$', Name, $'], 661 | add_record_or_macro_tag(EtsTags, File, Attr, Name, InnerPattern) 662 | end), 663 | ok. 664 | 665 | %%------------------------------------------------------------------------------ 666 | %% @doc Apply a function to the tags that that match a pattern. 667 | %% @end 668 | %%------------------------------------------------------------------------------ 669 | -spec scan_tags_core(Contents, Pattern, Fun) -> ok when 670 | Contents :: binary(), 671 | Pattern :: re:mp(), 672 | Fun :: fun(). 673 | scan_tags_core(Contents, Pattern, Fun) -> 674 | case re:run(Contents, Pattern, [{capture, all, binary}, global]) of 675 | nomatch -> 676 | ok; 677 | {match, Matches} -> 678 | lists:foreach(Fun, Matches) 679 | end. 680 | 681 | %%%============================================================================= 682 | %%% Add specific tags 683 | %%%============================================================================= 684 | 685 | %%------------------------------------------------------------------------------ 686 | %% @doc Add a tag about the file. 687 | %% 688 | %% If the file is a module, add a module tag. 689 | %% @end 690 | %%------------------------------------------------------------------------------ 691 | -spec add_file_tag(EtsTags, File, BaseName, ModName) -> ok when 692 | EtsTags :: ets:tid(), 693 | File :: file:filename(), 694 | BaseName :: string(), 695 | ModName :: string(). 696 | add_file_tag(EtsTags, File, BaseName, ModName) -> 697 | 698 | % File entry: 699 | % myfile.hrl ./myfile.hrl 1;" F 700 | % myfile.erl ./myfile.erl 1;" F 701 | add_tag(EtsTags, BaseName, File, "1", global, $F), 702 | 703 | case filename:extension(File) of 704 | ".erl" -> 705 | % Module entry: 706 | % myfile ./myfile.erl 1;" M 707 | add_tag(EtsTags, ModName, File, "1", global, $M); 708 | _ -> 709 | ok 710 | end. 711 | 712 | %%------------------------------------------------------------------------------ 713 | %% @doc Add a tag about a function definition. 714 | %% @end 715 | %%------------------------------------------------------------------------------ 716 | -spec add_func_tags(EtsTags, File, ModName, FuncName) -> ok when 717 | EtsTags :: ets:tid(), 718 | File :: file:filename(), 719 | ModName :: string(), 720 | FuncName :: binary(). 721 | add_func_tags(EtsTags, File, ModName, FuncName) -> 722 | 723 | log("Function definition found: ~s~n", [FuncName]), 724 | 725 | % Global entry: 726 | % mymod:f ./mymod.erl /^f\>/ 727 | add_tag(EtsTags, [ModName, ":", FuncName], File, ["/^", FuncName, "\\>/"], 728 | global, $f), 729 | 730 | % Static (or local) entry: 731 | % f ./mymod.erl /^f\>/ ;" file: 732 | add_tag(EtsTags, FuncName, File, ["/^", FuncName, "\\>/"], local, $f). 733 | 734 | %%------------------------------------------------------------------------------ 735 | %% @doc Add a tag about a type definition. 736 | %% @end 737 | %%------------------------------------------------------------------------------ 738 | -spec add_type_tags(EtsTags, File, ModName, Attribute, TypeName, 739 | InnerPattern) -> ok when 740 | EtsTags :: ets:tid(), 741 | File :: file:filename(), 742 | ModName :: string(), 743 | Attribute :: binary(), % "type" | "opaque" 744 | TypeName :: binary(), 745 | InnerPattern :: iolist(). 746 | add_type_tags(EtsTags, File, ModName, Attribute, TypeName, InnerPattern) -> 747 | 748 | log("Type definition found: ~s~n", [TypeName]), 749 | 750 | Pattern = ["/^-\\s\\*", Attribute, "\\s\\*", InnerPattern, $/], 751 | 752 | % Global entry: 753 | % mymod:mytype ./mymod.erl /^-type\s\*mytype\>/ 754 | % mymod:mytype ./mymod.erl /^-opaque\s\*mytype\>/ 755 | add_tag(EtsTags, [ModName, ":", TypeName], File, Pattern, global, $t), 756 | 757 | % Static (or local) entry: 758 | % mytype ./mymod.erl /^-type\s\*mytype\>/ 759 | % ;" file: 760 | % mytype ./mymod.erl /^-opaque\s\*mytype\>/ 761 | % ;" file: 762 | add_tag(EtsTags, TypeName, File, Pattern, local, $t). 763 | 764 | %%------------------------------------------------------------------------------ 765 | %% @doc Add a tag about a record or macro. 766 | %% @end 767 | %%------------------------------------------------------------------------------ 768 | -spec add_record_or_macro_tag(EtsTags, File, Attribute, Name, 769 | InnerPattern) -> ok when 770 | EtsTags :: ets:tid(), 771 | File :: file:filename(), 772 | Attribute :: binary(), % "record" | "macro" 773 | Name :: binary(), % the name of the record or macro 774 | InnerPattern :: iolist(). 775 | add_record_or_macro_tag(EtsTags, File, Attribute, Name, InnerPattern) -> 776 | 777 | {Kind, Prefix} = 778 | case Attribute of 779 | <<"record">> -> 780 | log("Record found: ~s~n", [Name]), 781 | {$r, $#}; 782 | <<"define">> -> 783 | log("Macro found: ~s~n", [Name]), 784 | {$d, $?} 785 | end, 786 | 787 | Scope = 788 | case filename:extension(File) of 789 | ".hrl" -> 790 | global; 791 | _ -> 792 | local 793 | end, 794 | 795 | % myrec ./mymod.erl /^-record\s\*\/;" r file: 796 | % myrec ./myhrl.hrl /^-record\s\*\/;" r 797 | % mymac ./mymod.erl /^-define\s\*\/;" m file: 798 | % mymac ./myhrl.hrl /^-define\s\*\/;" m 799 | add_tag(EtsTags, Name, File, 800 | ["/^-\\s\\*", Attribute, "\\s\\*(\\?\\s\\*", InnerPattern, "/"], 801 | Scope, Kind), 802 | 803 | % #myrec ./mymod.erl /^-record\s\*\/;" r file: 804 | % #myrec ./myhrl.hrl /^-record\s\*\/;" r 805 | % ?mymac ./mymod.erl /^-define\s\*\/;" m file: 806 | % ?mymac ./myhrl.hrl /^-define\s\*\/;" m 807 | add_tag(EtsTags, [Prefix|Name], File, 808 | ["/^-\\s\\*", Attribute, "\\s\\*(\\?\\s\\*", InnerPattern, "/"], 809 | Scope, Kind). 810 | 811 | %%------------------------------------------------------------------------------ 812 | %% @doc Add a tags to the ETS table. 813 | %% @end 814 | %%------------------------------------------------------------------------------ 815 | -spec add_tag(EtsTags, TagName, File, TagAddress, Scope, Kind) -> ok when 816 | EtsTags :: ets:tid(), 817 | TagName :: iodata(), 818 | File :: file:filename(), 819 | TagAddress :: tag_address_iolist(), 820 | Scope :: scope(), 821 | Kind :: char(). 822 | add_tag(EtsTags, TagName, File, TagAddress, Scope, Kind) -> 823 | _ = ets:insert_new(EtsTags, 824 | {{iolist_to_binary(TagName), 825 | unicode:characters_to_binary(File), 826 | Scope, 827 | Kind}, 828 | iolist_to_binary(TagAddress)}), 829 | ok. 830 | 831 | %%%============================================================================= 832 | %%% Writing tags into a file 833 | %%%============================================================================= 834 | 835 | %%------------------------------------------------------------------------------ 836 | %% @doc Write the tags into a tag file. 837 | %% 838 | %% See `:help tags-file-format' for the tag file format. 839 | %% @end 840 | %%------------------------------------------------------------------------------ 841 | -spec tags_to_file(EtsTags, TagsFile) -> ok when 842 | EtsTags :: ets:tid(), 843 | TagsFile :: file:name_all(). 844 | tags_to_file(EtsTags, TagsFile) -> 845 | Header = "!_TAG_FILE_SORTED\t1\t/0=unsorted, 1=sorted/\n", 846 | TagList = ets:tab2list(EtsTags), 847 | TagListSorted = lists:sort(fun should_first_tag_come_earlier/2, TagList), 848 | Entries = [tag_to_binary(Tag) || Tag <- TagListSorted], 849 | file:write_file(TagsFile, [Header, Entries]), 850 | ok. 851 | 852 | %%------------------------------------------------------------------------------ 853 | %% @doc Return whether the first tag should come earlier in the tag file than 854 | %% the second tag. 855 | %% @end 856 | %%------------------------------------------------------------------------------ 857 | -spec should_first_tag_come_earlier(Tag1, Tag2) -> Result when 858 | Tag1 :: {{TagName, File, Scope, Kind}, TagAddress}, 859 | Tag2 :: {{TagName, File, Scope, Kind}, TagAddress}, 860 | TagName :: binary(), 861 | File :: binary(), 862 | Scope :: scope(), 863 | Kind :: char(), 864 | TagAddress :: tag_address_binary(), 865 | Result :: boolean(). 866 | should_first_tag_come_earlier(TagA = {{TagName, FilePathA, _, _}, _}, 867 | TagB = {{TagName, FilePathB, _, _}, _}) -> 868 | % The two tags have the same TagName, so if one of the tags is in a `_build' 869 | % directory, that should come later. 870 | % 871 | % Explanation: It can happen that the same tag is present in the tag list 872 | % twice: from the `app' directory and from the `_build' directory. 873 | % 874 | % * On Unix: rebar3 symlinks the applications in the `app` directory into 875 | % the `_build' directory. If `vim_erlang_tags.erl' is executed with 876 | % `--follow', it follows those symlinks and the tags from the source 877 | % files are present in the tag list twice. 878 | % 879 | % * On Windows: rebar3 copies the applications in the `app` directory into 880 | % the `_build' directory. The tags from the source files are present in 881 | % the tag list twice. 882 | % 883 | % If this happens, we want Vim to give precedence to the tags coming from 884 | % `app'. 885 | case {is_in_build_dir(FilePathA), 886 | is_in_build_dir(FilePathB)} of 887 | {false, true} -> 888 | % TagA is not in _build dir; so it should be earlier in the tag file 889 | % than TagB; so it should be earlier in the `Entries 'list; so it 890 | % should be considered smaller by `lists:sort'; so this function 891 | % should return `true'. 892 | true; 893 | {true, false} -> 894 | % Opposite reasoning to the previous branch. 895 | false; 896 | _ -> 897 | TagA =< TagB 898 | end; 899 | should_first_tag_come_earlier(TagA, TagB) -> 900 | TagA =< TagB. 901 | 902 | %%------------------------------------------------------------------------------ 903 | %% @doc Return whether FilePath contains a `_build' component. 904 | %% @end 905 | %%------------------------------------------------------------------------------ 906 | -spec is_in_build_dir(FilePath) -> Result when 907 | FilePath :: binary(), 908 | Result :: boolean(). 909 | is_in_build_dir(FilePath) -> 910 | re:run(FilePath, ?RE_BUILD, [{capture, none}, global]) =/= nomatch. 911 | 912 | %%------------------------------------------------------------------------------ 913 | %% @doc Convert one tag into a line in a tag file. 914 | %% @end 915 | %%------------------------------------------------------------------------------ 916 | -spec tag_to_binary({{TagName, File, Scope, Kind}, TagAddress}) -> Result when 917 | TagName :: iodata(), 918 | File :: binary(), 919 | Scope :: scope(), 920 | Kind :: char(), 921 | TagAddress :: tag_address_binary(), 922 | Result :: binary(). 923 | tag_to_binary({{TagName, File, Scope, Kind}, TagAddress}) -> 924 | ScopeStr = 925 | case Scope of 926 | global -> ""; 927 | local -> "\tfile:" 928 | end, 929 | iolist_to_binary([TagName, "\t", 930 | File, "\t", 931 | TagAddress, ";\"\t", 932 | Kind, 933 | ScopeStr, "\n"]). 934 | 935 | %%%============================================================================= 936 | %%% Utility functions 937 | %%%============================================================================= 938 | 939 | %%------------------------------------------------------------------------------ 940 | %% @doc Print a log entry. 941 | %% @end 942 | %%------------------------------------------------------------------------------ 943 | -spec log(Format) -> ok when 944 | Format :: io:format(). 945 | log(Format) -> 946 | log(Format, []). 947 | 948 | %%------------------------------------------------------------------------------ 949 | %% @doc Print a log entry. 950 | %% @end 951 | %%------------------------------------------------------------------------------ 952 | -spec log(Format, Data) -> ok when 953 | Format :: io:format(), 954 | Data :: [any()]. 955 | log(Format, Data) -> 956 | case get(verbose) of 957 | true -> 958 | io:format(Format, Data); 959 | _ -> 960 | ok 961 | end. 962 | 963 | %%------------------------------------------------------------------------------ 964 | %% @doc Print an error. 965 | %% @end 966 | %%------------------------------------------------------------------------------ 967 | -spec log_error(Format, Data) -> ok when 968 | Format :: io:format(), 969 | Data :: [any()]. 970 | log_error(Format, Data) -> 971 | io:format(standard_error, Format, Data). 972 | 973 | %%------------------------------------------------------------------------------ 974 | %% @doc Print the script's help. 975 | %% 976 | %% This is the last function so that it's easy to jump to it. 977 | %% @end 978 | %%------------------------------------------------------------------------------ 979 | -spec print_help() -> ok. 980 | print_help() -> 981 | Help = 982 | "Usage: vim_erlang_tags.erl [-h|--help] [-v|--verbose] [-o|--output FILE] 983 | [-i|--include FILE_WILDCARD] 984 | [-g|--ignore FILE_WILDCARD] 985 | [--follow] [-p|--otp] 986 | DIR_OR_FILE... 987 | 988 | Description: 989 | vim_erlang_tags.erl creates a tags file that can be used by Vim. The 990 | directories given as arguments are searched (recursively) for *.erl and *.hrl 991 | files, which will be scanned. The files given as arguments are also scanned. 992 | The default is to search in the current directory. 993 | 994 | Options: 995 | -h, --help Print help and exit. 996 | -v, --verbose Verbose output. 997 | -o, --output FILE 998 | Write the output into the given file instead of ./tags. 999 | -i, --include FILE_WILDCARD 1000 | -g, --ignore FILE_WILDCARD 1001 | Include or ignore the files/directories that match the given wildcard. 1002 | Read http://www.erlang.org/doc/man/filelib.html#wildcard-1 for 1003 | the wildcard patterns. 1004 | --follow Follow symbolic links 1005 | -p, --otp Include the currently used OTP lib_dir 1006 | 1007 | Examples: 1008 | $ vim_erlang_tags.erl 1009 | $ vim_erlang_tags.erl . # Same 1010 | $ vim_erlang_tags.erl /path/to/project1 /path/to/project2 1011 | ", 1012 | io:format("~s", [Help]). 1013 | -------------------------------------------------------------------------------- /plugin/vim-erlang-tags.vim: -------------------------------------------------------------------------------- 1 | " Copyright 2013 Csaba Hoch 2 | " Copyright 2013 Adam Rutkowski 3 | " 4 | " Licensed under the Apache License, Version 2.0 (the "License"); 5 | " you may not use this file except in compliance with the License. 6 | " You may obtain a copy of the License at 7 | " 8 | " http://www.apache.org/licenses/LICENSE-2.0 9 | " 10 | " Unless required by applicable law or agreed to in writing, software 11 | " distributed under the License is distributed on an "AS IS" BASIS, 12 | " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | " See the License for the specific language governing permissions and 14 | " limitations under the License. 15 | 16 | if exists("b:vim_erlang_tags_loaded") 17 | finish 18 | else 19 | let b:vim_erlang_tags_loaded = 1 20 | endif 21 | 22 | autocmd FileType erlang call VimErlangTagsDefineMappings() 23 | 24 | command! ErlangTags call vim_erlang_tags#VimErlangTags() 25 | 26 | if exists("g:erlang_tags_auto_update") && g:erlang_tags_auto_update == 1 27 | au BufWritePost *.erl,*.hrl call vim_erlang_tags#AsyncVimErlangTags() 28 | endif 29 | 30 | if exists("g:erlang_tags_auto_update_current") && g:erlang_tags_auto_update_current == 1 31 | augroup vimErlangMaps 32 | au! 33 | autocmd BufWritePost *.erl,*.hrl call vim_erlang_tags#UpdateTags() 34 | augroup end 35 | endif 36 | 37 | function! VimErlangTagsDefineMappings() 38 | nnoremap :call vim_erlang_tags#VimErlangTagsSelect(0) 39 | nnoremap g :call vim_erlang_tags#VimErlangTagsSelect(0) 40 | nnoremap :call vim_erlang_tags#VimErlangTagsSelect(0) 41 | nnoremap g] :call vim_erlang_tags#VimErlangTagsSelect(0)g] 42 | nnoremap g :call vim_erlang_tags#VimErlangTagsSelect(0)g 43 | nnoremap :call vim_erlang_tags#VimErlangTagsSelect(1) 44 | nnoremap ] :call vim_erlang_tags#VimErlangTagsSelect(1) 45 | nnoremap g :call vim_erlang_tags#VimErlangTagsSelect(1)g 46 | endfunction 47 | --------------------------------------------------------------------------------