Introduces the `format` Python module which provides `clang_format()` and `yapf()` functions which efficiently (compared to vimscript) invoke `clang-format` or `yapf` respectively then apply the minimal number of changes using `difflib.SequenceMatcher`. Additionally, in order to invoke these Python functions add |autoload| functions `format#clang_format()` and `format#yapf()` which can be directly used by the 'formatexpr' setting. Finally, add |ftplugin| files which set 'formatexpr' when the |autoload| functions are available.
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""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]
|