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