From 46d27c17fd938070e0f5a5d7b7e29b46713b9f4c Mon Sep 17 00:00:00 2001 From: "Kenneth Benzie (Benie)" Date: Wed, 24 Mar 2021 23:37:19 +0000 Subject: [PATCH] Move formatexpr config in-tree, out of plugin 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. --- autoload/format.vim | 14 +++++ ftplugin/c.vim | 3 ++ ftplugin/cpp.vim | 3 ++ ftplugin/java.vim | 3 ++ ftplugin/javascript.vim | 3 ++ ftplugin/objc.vim | 3 ++ ftplugin/objcpp.vim | 3 ++ ftplugin/proto.vim | 3 ++ ftplugin/python.vim | 3 ++ plugin/format.vim | 8 +++ pythonx/format.py | 117 ++++++++++++++++++++++++++++++++++++++++ vimrc | 3 -- 12 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 autoload/format.vim create mode 100644 ftplugin/c.vim create mode 100644 ftplugin/cpp.vim create mode 100644 ftplugin/java.vim create mode 100644 ftplugin/javascript.vim create mode 100644 ftplugin/objc.vim create mode 100644 ftplugin/objcpp.vim create mode 100644 ftplugin/proto.vim create mode 100644 ftplugin/python.vim create mode 100644 plugin/format.vim create mode 100644 pythonx/format.py diff --git a/autoload/format.vim b/autoload/format.vim new file mode 100644 index 0000000..2667ca1 --- /dev/null +++ b/autoload/format.vim @@ -0,0 +1,14 @@ +if !has('pythonx') + finish +endif + +" set debug=msg,throw +pythonx import format + +function! format#clang_format() abort + pythonx format.clang_format() +endfunction + +function! format#yapf() abort + pythonx format.yapf() +endfunction diff --git a/ftplugin/c.vim b/ftplugin/c.vim new file mode 100644 index 0000000..9dc43db --- /dev/null +++ b/ftplugin/c.vim @@ -0,0 +1,3 @@ +if has('pythonx') + set formatexpr=format#clang_format() +endif diff --git a/ftplugin/cpp.vim b/ftplugin/cpp.vim new file mode 100644 index 0000000..9dc43db --- /dev/null +++ b/ftplugin/cpp.vim @@ -0,0 +1,3 @@ +if has('pythonx') + set formatexpr=format#clang_format() +endif diff --git a/ftplugin/java.vim b/ftplugin/java.vim new file mode 100644 index 0000000..9dc43db --- /dev/null +++ b/ftplugin/java.vim @@ -0,0 +1,3 @@ +if has('pythonx') + set formatexpr=format#clang_format() +endif diff --git a/ftplugin/javascript.vim b/ftplugin/javascript.vim new file mode 100644 index 0000000..9dc43db --- /dev/null +++ b/ftplugin/javascript.vim @@ -0,0 +1,3 @@ +if has('pythonx') + set formatexpr=format#clang_format() +endif diff --git a/ftplugin/objc.vim b/ftplugin/objc.vim new file mode 100644 index 0000000..9dc43db --- /dev/null +++ b/ftplugin/objc.vim @@ -0,0 +1,3 @@ +if has('pythonx') + set formatexpr=format#clang_format() +endif diff --git a/ftplugin/objcpp.vim b/ftplugin/objcpp.vim new file mode 100644 index 0000000..9dc43db --- /dev/null +++ b/ftplugin/objcpp.vim @@ -0,0 +1,3 @@ +if has('pythonx') + set formatexpr=format#clang_format() +endif diff --git a/ftplugin/proto.vim b/ftplugin/proto.vim new file mode 100644 index 0000000..9dc43db --- /dev/null +++ b/ftplugin/proto.vim @@ -0,0 +1,3 @@ +if has('pythonx') + set formatexpr=format#clang_format() +endif diff --git a/ftplugin/python.vim b/ftplugin/python.vim new file mode 100644 index 0000000..b7b6a68 --- /dev/null +++ b/ftplugin/python.vim @@ -0,0 +1,3 @@ +if has('pythonx') + set formatexpr=format#yapf() +endif diff --git a/plugin/format.vim b/plugin/format.vim new file mode 100644 index 0000000..e5b073c --- /dev/null +++ b/plugin/format.vim @@ -0,0 +1,8 @@ +if !has('pythonx') + finish +endif + +let g:clang_format_path = get(g:, 'clang_format_path', 'clang-format') +let g:clang_format_style = get(g:, 'clang_format_style', 'google') +let g:yapf_path = get(g:, 'yapf_path', 'yapf') +let g:yapf_style = get(g:, 'yapf_style', 'pep8') diff --git a/pythonx/format.py b/pythonx/format.py new file mode 100644 index 0000000..2ceebc2 --- /dev/null +++ b/pythonx/format.py @@ -0,0 +1,117 @@ +"""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] diff --git a/vimrc b/vimrc index bd718d1..c87ec93 100644 --- a/vimrc +++ b/vimrc @@ -59,9 +59,6 @@ let g:ale_cmake_cmakelint_options = " Version control differences in the sign column Pack 'mhinz/vim-signify' -" format.vim - format with text objects -Pack 'git@bitbucket.org:infektor/format.vim.git' - " vim-textobj-user - library for creating text objects Pack 'kana/vim-textobj-user' " vim-textobj-entire - Entire file text object