| # This file is a minimal clang-include-fixer vim-integration. To install: |
| # - Change 'binary' if clang-include-fixer is not on the path (see below). |
| # - Add to your .vimrc: |
| # |
| # noremap <leader>cf :pyf path/to/llvm/source/tools/clang/tools/extra/clang-include-fixer/tool/clang-include-fixer.py<cr> |
| # |
| # This enables clang-include-fixer for NORMAL and VISUAL mode. Change |
| # "<leader>cf" to another binding if you need clang-include-fixer on a |
| # different key. |
| # |
| # To set up clang-include-fixer, see |
| # http://clang.llvm.org/extra/clang-include-fixer.html |
| # |
| # With this integration you can press the bound key and clang-include-fixer will |
| # be run on the current buffer. |
| # |
| # It operates on the current, potentially unsaved buffer and does not create |
| # or save any files. To revert a fix, just undo. |
| |
| from __future__ import print_function |
| import argparse |
| import difflib |
| import json |
| import re |
| import subprocess |
| import vim |
| |
| # set g:clang_include_fixer_path to the path to clang-include-fixer if it is not |
| # on the path. |
| # Change this to the full path if clang-include-fixer is not on the path. |
| binary = "clang-include-fixer" |
| if vim.eval('exists("g:clang_include_fixer_path")') == "1": |
| binary = vim.eval("g:clang_include_fixer_path") |
| |
| maximum_suggested_headers = 3 |
| if vim.eval('exists("g:clang_include_fixer_maximum_suggested_headers")') == "1": |
| maximum_suggested_headers = max( |
| 1, vim.eval("g:clang_include_fixer_maximum_suggested_headers") |
| ) |
| |
| increment_num = 5 |
| if vim.eval('exists("g:clang_include_fixer_increment_num")') == "1": |
| increment_num = max(1, vim.eval("g:clang_include_fixer_increment_num")) |
| |
| jump_to_include = False |
| if vim.eval('exists("g:clang_include_fixer_jump_to_include")') == "1": |
| jump_to_include = vim.eval("g:clang_include_fixer_jump_to_include") != "0" |
| |
| query_mode = False |
| if vim.eval('exists("g:clang_include_fixer_query_mode")') == "1": |
| query_mode = vim.eval("g:clang_include_fixer_query_mode") != "0" |
| |
| |
| def GetUserSelection(message, headers, maximum_suggested_headers): |
| eval_message = message + "\n" |
| for idx, header in enumerate(headers[0:maximum_suggested_headers]): |
| eval_message += "({0}). {1}\n".format(idx + 1, header) |
| eval_message += "Enter (q) to quit;" |
| if maximum_suggested_headers < len(headers): |
| eval_message += " (m) to show {0} more candidates.".format( |
| min(increment_num, len(headers) - maximum_suggested_headers) |
| ) |
| |
| eval_message += "\nSelect (default 1): " |
| res = vim.eval("input('{0}')".format(eval_message)) |
| if res == "": |
| # choose the top ranked header by default |
| idx = 1 |
| elif res == "q": |
| raise Exception(" Insertion cancelled...") |
| elif res == "m": |
| return GetUserSelection( |
| message, headers, maximum_suggested_headers + increment_num |
| ) |
| else: |
| try: |
| idx = int(res) |
| if idx <= 0 or idx > len(headers): |
| raise Exception() |
| except Exception: |
| # Show a new prompt on invalid option instead of aborting so that users |
| # don't need to wait for another clang-include-fixer run. |
| print("Invalid option: {}".format(res), file=sys.stderr) |
| return GetUserSelection(message, headers, maximum_suggested_headers) |
| return headers[idx - 1] |
| |
| |
| def execute(command, text): |
| # Avoid flashing a cmd prompt on Windows. |
| startupinfo = None |
| if sys.platform.startswith("win32"): |
| startupinfo = subprocess.STARTUPINFO() |
| startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW |
| startupinfo.wShowWindow = subprocess.SW_HIDE |
| |
| p = subprocess.Popen( |
| command, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| stdin=subprocess.PIPE, |
| startupinfo=startupinfo, |
| ) |
| return p.communicate(input=text.encode("utf-8")) |
| |
| |
| def InsertHeaderToVimBuffer(header, text): |
| command = [ |
| binary, |
| "-stdin", |
| "-insert-header=" + json.dumps(header), |
| vim.current.buffer.name, |
| ] |
| stdout, stderr = execute(command, text) |
| if stderr: |
| raise Exception(stderr) |
| if stdout: |
| lines = stdout.splitlines() |
| sequence = difflib.SequenceMatcher(None, vim.current.buffer, lines) |
| line_num = None |
| for op in reversed(sequence.get_opcodes()): |
| if op[0] != "equal": |
| vim.current.buffer[op[1] : op[2]] = lines[op[3] : op[4]] |
| if op[0] == "insert": |
| # line_num in vim is 1-based. |
| line_num = op[1] + 1 |
| |
| if jump_to_include and line_num: |
| vim.current.window.cursor = (line_num, 0) |
| |
| |
| # The vim internal implementation (expand("cword"/"cWORD")) doesn't support |
| # our use case very well, we re-implement our own one. |
| def get_symbol_under_cursor(): |
| line = vim.eval('line(".")') |
| # column number in vim is 1-based. |
| col = int(vim.eval('col(".")')) - 1 |
| line_text = vim.eval("getline({0})".format(line)) |
| if len(line_text) == 0: |
| return "" |
| symbol_pos_begin = col |
| p = re.compile("[a-zA-Z0-9:_]") |
| while symbol_pos_begin >= 0 and p.match(line_text[symbol_pos_begin]): |
| symbol_pos_begin -= 1 |
| |
| symbol_pos_end = col |
| while symbol_pos_end < len(line_text) and p.match(line_text[symbol_pos_end]): |
| symbol_pos_end += 1 |
| return line_text[symbol_pos_begin + 1 : symbol_pos_end] |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Vim integration for clang-include-fixer" |
| ) |
| parser.add_argument("-db", default="yaml", help="clang-include-fixer input format.") |
| parser.add_argument("-input", default="", help="String to initialize the database.") |
| # Don't throw exception when parsing unknown arguments to make the script |
| # work in neovim. |
| # Neovim (at least v0.2.1) somehow mangles the sys.argv in a weird way: it |
| # will pass additional arguments (e.g. "-c script_host.py") to sys.argv, |
| # which makes the script fail. |
| args, _ = parser.parse_known_args() |
| |
| # Get the current text. |
| buf = vim.current.buffer |
| text = "\n".join(buf) |
| |
| if query_mode: |
| symbol = get_symbol_under_cursor() |
| if len(symbol) == 0: |
| print("Skip querying empty symbol.") |
| return |
| command = [ |
| binary, |
| "-stdin", |
| "-query-symbol=" + get_symbol_under_cursor(), |
| "-db=" + args.db, |
| "-input=" + args.input, |
| vim.current.buffer.name, |
| ] |
| else: |
| # Run command to get all headers. |
| command = [ |
| binary, |
| "-stdin", |
| "-output-headers", |
| "-db=" + args.db, |
| "-input=" + args.input, |
| vim.current.buffer.name, |
| ] |
| stdout, stderr = execute(command, text) |
| if stderr: |
| print( |
| "Error while running clang-include-fixer: {}".format(stderr), |
| file=sys.stderr, |
| ) |
| return |
| |
| include_fixer_context = json.loads(stdout) |
| query_symbol_infos = include_fixer_context["QuerySymbolInfos"] |
| if not query_symbol_infos: |
| print("The file is fine, no need to add a header.") |
| return |
| symbol = query_symbol_infos[0]["RawIdentifier"] |
| # The header_infos is already sorted by clang-include-fixer. |
| header_infos = include_fixer_context["HeaderInfos"] |
| # Deduplicate headers while keeping the order, so that the same header would |
| # not be suggested twice. |
| unique_headers = [] |
| seen = set() |
| for header_info in header_infos: |
| header = header_info["Header"] |
| if header not in seen: |
| seen.add(header) |
| unique_headers.append(header) |
| |
| if not unique_headers: |
| print("Couldn't find a header for {0}.".format(symbol)) |
| return |
| |
| try: |
| selected = unique_headers[0] |
| inserted_header_infos = header_infos |
| if len(unique_headers) > 1: |
| selected = GetUserSelection( |
| "choose a header file for {0}.".format(symbol), |
| unique_headers, |
| maximum_suggested_headers, |
| ) |
| inserted_header_infos = [ |
| header for header in header_infos if header["Header"] == selected |
| ] |
| include_fixer_context["HeaderInfos"] = inserted_header_infos |
| |
| InsertHeaderToVimBuffer(include_fixer_context, text) |
| print("Added #include {0} for {1}.".format(selected, symbol)) |
| except Exception as error: |
| print(error, file=sys.stderr) |
| return |
| |
| |
| if __name__ == "__main__": |
| main() |