1 # This file is a minimal clang-include-fixer vim-integration. To install: 2 # - Change 'binary' if clang-include-fixer is not on the path (see below). 3 # - Add to your .vimrc: 4 # 5 # noremap <leader>cf :pyf path/to/llvm/source/tools/clang/tools/extra/include-fixer/tool/clang-include-fixer.py<cr> 6 # 7 # This enables clang-include-fixer for NORMAL and VISUAL mode. Change "<leader>cf" 8 # to another binding if you need clang-include-fixer on a different key. 9 # 10 # To set up clang-include-fixer, see http://clang.llvm.org/extra/include-fixer.html 11 # 12 # With this integration you can press the bound key and clang-include-fixer will 13 # be run on the current buffer. 14 # 15 # It operates on the current, potentially unsaved buffer and does not create 16 # or save any files. To revert a fix, just undo. 17 18 import argparse 19 import difflib 20 import json 21 import re 22 import subprocess 23 import vim 24 25 # set g:clang_include_fixer_path to the path to clang-include-fixer if it is not 26 # on the path. 27 # Change this to the full path if clang-include-fixer is not on the path. 28 binary = 'clang-include-fixer' 29 if vim.eval('exists("g:clang_include_fixer_path")') == "1": 30 binary = vim.eval('g:clang_include_fixer_path') 31 32 maximum_suggested_headers = 3 33 if vim.eval('exists("g:clang_include_fixer_maximum_suggested_headers")') == "1": 34 maximum_suggested_headers = max( 35 1, 36 vim.eval('g:clang_include_fixer_maximum_suggested_headers')) 37 38 increment_num = 5 39 if vim.eval('exists("g:clang_include_fixer_increment_num")') == "1": 40 increment_num = max( 41 1, 42 vim.eval('g:clang_include_fixer_increment_num')) 43 44 jump_to_include = False 45 if vim.eval('exists("g:clang_include_fixer_jump_to_include")') == "1": 46 jump_to_include = vim.eval('g:clang_include_fixer_jump_to_include') != "0" 47 48 query_mode = False 49 if vim.eval('exists("g:clang_include_fixer_query_mode")') == "1": 50 query_mode = vim.eval('g:clang_include_fixer_query_mode') != "0" 51 52 53 def GetUserSelection(message, headers, maximum_suggested_headers): 54 eval_message = message + '\n' 55 for idx, header in enumerate(headers[0:maximum_suggested_headers]): 56 eval_message += "({0}). {1}\n".format(idx + 1, header) 57 eval_message += "Enter (q) to quit;" 58 if maximum_suggested_headers < len(headers): 59 eval_message += " (m) to show {0} more candidates.".format( 60 min(increment_num, len(headers) - maximum_suggested_headers)) 61 62 eval_message += "\nSelect (default 1): " 63 res = vim.eval("input('{0}')".format(eval_message)) 64 if res == '': 65 # choose the top ranked header by default 66 idx = 1 67 elif res == 'q': 68 raise Exception(' Insertion cancelled...') 69 elif res == 'm': 70 return GetUserSelection(message, 71 headers, maximum_suggested_headers + increment_num) 72 else: 73 try: 74 idx = int(res) 75 if idx <= 0 or idx > len(headers): 76 raise Exception() 77 except Exception: 78 # Show a new prompt on invalid option instead of aborting so that users 79 # don't need to wait for another include-fixer run. 80 print >> sys.stderr, "Invalid option:", res 81 return GetUserSelection(message, headers, maximum_suggested_headers) 82 return headers[idx - 1] 83 84 85 def execute(command, text): 86 p = subprocess.Popen(command, 87 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 88 stdin=subprocess.PIPE) 89 return p.communicate(input=text) 90 91 92 def InsertHeaderToVimBuffer(header, text): 93 command = [binary, "-stdin", "-insert-header=" + json.dumps(header), 94 vim.current.buffer.name] 95 stdout, stderr = execute(command, text) 96 if stderr: 97 raise Exception(stderr) 98 if stdout: 99 lines = stdout.splitlines() 100 sequence = difflib.SequenceMatcher(None, vim.current.buffer, lines) 101 line_num = None 102 for op in reversed(sequence.get_opcodes()): 103 if op[0] != 'equal': 104 vim.current.buffer[op[1]:op[2]] = lines[op[3]:op[4]] 105 if op[0] == 'insert': 106 # line_num in vim is 1-based. 107 line_num = op[1] + 1 108 109 if jump_to_include and line_num: 110 vim.current.window.cursor = (line_num, 0) 111 112 113 # The vim internal implementation (expand("cword"/"cWORD")) doesn't support 114 # our use case very well, we re-implement our own one. 115 def get_symbol_under_cursor(): 116 line = vim.eval("line(\".\")") 117 # column number in vim is 1-based. 118 col = int(vim.eval("col(\".\")")) - 1 119 line_text = vim.eval("getline({0})".format(line)) 120 if len(line_text) == 0: return "" 121 symbol_pos_begin = col 122 p = re.compile('[a-zA-Z0-9:_]') 123 while symbol_pos_begin >= 0 and p.match(line_text[symbol_pos_begin]): 124 symbol_pos_begin -= 1 125 126 symbol_pos_end = col 127 while symbol_pos_end < len(line_text) and p.match(line_text[symbol_pos_end]): 128 symbol_pos_end += 1 129 return line_text[symbol_pos_begin+1:symbol_pos_end] 130 131 132 def main(): 133 parser = argparse.ArgumentParser( 134 description='Vim integration for clang-include-fixer') 135 parser.add_argument('-db', default='yaml', 136 help='clang-include-fixer input format.') 137 parser.add_argument('-input', default='', 138 help='String to initialize the database.') 139 # Don't throw exception when parsing unknown arguements to make the script 140 # work in neovim. 141 # Neovim (at least v0.2.1) somehow mangles the sys.argv in a weird way: it 142 # will pass additional arguments (e.g. "-c script_host.py") to sys.argv, 143 # which makes the script fail. 144 args, _ = parser.parse_known_args() 145 146 # Get the current text. 147 buf = vim.current.buffer 148 text = '\n'.join(buf) 149 150 if query_mode: 151 symbol = get_symbol_under_cursor() 152 if len(symbol) == 0: 153 print "Skip querying empty symbol." 154 return 155 command = [binary, "-stdin", "-query-symbol="+get_symbol_under_cursor(), 156 "-db=" + args.db, "-input=" + args.input, 157 vim.current.buffer.name] 158 else: 159 # Run command to get all headers. 160 command = [binary, "-stdin", "-output-headers", "-db=" + args.db, 161 "-input=" + args.input, vim.current.buffer.name] 162 stdout, stderr = execute(command, text) 163 if stderr: 164 print >> sys.stderr, "Error while running clang-include-fixer: " + stderr 165 return 166 167 include_fixer_context = json.loads(stdout) 168 query_symbol_infos = include_fixer_context["QuerySymbolInfos"] 169 if not query_symbol_infos: 170 print "The file is fine, no need to add a header." 171 return 172 symbol = query_symbol_infos[0]["RawIdentifier"] 173 # The header_infos is already sorted by include-fixer. 174 header_infos = include_fixer_context["HeaderInfos"] 175 # Deduplicate headers while keeping the order, so that the same header would 176 # not be suggested twice. 177 unique_headers = [] 178 seen = set() 179 for header_info in header_infos: 180 header = header_info["Header"] 181 if header not in seen: 182 seen.add(header) 183 unique_headers.append(header) 184 185 if not unique_headers: 186 print "Couldn't find a header for {0}.".format(symbol) 187 return 188 189 try: 190 selected = unique_headers[0] 191 inserted_header_infos = header_infos 192 if len(unique_headers) > 1: 193 selected = GetUserSelection( 194 "choose a header file for {0}.".format(symbol), 195 unique_headers, maximum_suggested_headers) 196 inserted_header_infos = [ 197 header for header in header_infos if header["Header"] == selected] 198 include_fixer_context["HeaderInfos"] = inserted_header_infos 199 200 InsertHeaderToVimBuffer(include_fixer_context, text) 201 print "Added #include {0} for {1}.".format(selected, symbol) 202 except Exception as error: 203 print >> sys.stderr, error.message 204 return 205 206 207 if __name__ == '__main__': 208 main() 209