"""Python formatexpr for clang-format & yapf""" import json import sys import subprocess import difflib import vim class FormatError(Exception): """A formatting error.""" def clang_format(): """Call clang-format on the current text object.""" clang_format = vim.vars['clang_format_path'] # TODO: The cursor position before gq is invoked is not preserved in the # jump list, this expression will return the cursor position at the # beginning of the motion or text object. # Is there a way to get the cursor position immediately before gq is # invoked? So that we can pass the position to clang-format in order for # it to return the updated position to continue editing. # Query the current absolute cursor positon. cursor = int(vim.eval('line2byte(".") + col(".")')) - 2 if cursor < 0: return # Determine the lines to format. startline = int(vim.eval('v:lnum')) endline = startline + int(vim.eval('v:count')) - 1 lines = f'{startline}:{endline}' fallback_style = vim.vars['clang_format_style'] # Construct the clang-format command to call. command = [ clang_format, '-style', 'file', '-cursor', str(cursor), '-lines', lines, '-fallback-style', fallback_style ] if vim.current.buffer.name: command += ['-assume-filename', vim.current.buffer.name] # Call the clang-format command. output = call(command) if not output: return # Read the clang-format json header. header = json.loads(output[0]) if header.get('IncompleteFormat'): raise FormatError('clang-format: incomplete (syntax errors).') # Replace the formatted regions. replace_regions(output[1:]) # Set the updated cursor position. vim.command('goto %d' % (header['Cursor'] + 1)) def yapf(): """Call yapf on the current text object.""" yapf = vim.vars['yapf_path'] # Determine the lines to format. start = int(vim.eval('v:lnum')) end = start + int(vim.eval('v:count')) lines = '{0}-{1}'.format(start, end) style = vim.vars['yapf_style'] # Construct the clang-format command to call. command = [yapf, '--style', style, '--lines', lines] # TODO: Since yapf is a Python package, we could import the module and # call it directly instead. It would then be possible to output better # error messages and act on the buffer directly. # Call the yapf command. output = call(command) if not output: return # Replace the formatted regions. replace_regions(output[:-1]) def call(command): """Call the command to format the text. Arguments: command (list): Formatting command to call. text (str): Text to be passed to stdin of command. Returns: list: The output of the formatter split on new lines. None: If the subprocess failed. """ # Don't open a cmd prompt window on Windows. startupinfo = None if sys.platform.startswith('win32'): startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE # Call the formatting command. process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, startupinfo=startupinfo) stdout, stderr = process.communicate( input='\n'.join(vim.current.buffer).encode('utf-8')) if stderr: raise FormatError(stderr) if not stdout: raise FormatError('No output from {0}.'.format(command[0])) # Split the lines into a list of elements. return stdout.decode('utf-8').split('\n') def replace_regions(lines): """Replace formatted regions in the current buffer. Arguments: lines (list): The formatted buffer lines. """ matcher = difflib.SequenceMatcher(None, vim.current.buffer, lines) for tag, i1, i2, j1, j2 in reversed(matcher.get_opcodes()): if tag != 'equal': vim.current.buffer[i1:i2] = lines[j1:j2]