From 9960006656ce7e59f556b9190be150c9bf93045c Mon Sep 17 00:00:00 2001
From: "Kenneth Benzie (Benie)" <benie@infektor.net>
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/__pycache__/format.cpython-39.pyc | Bin 0 -> 3001 bytes
 pythonx/format.py                         | 117 ++++++++++++++++++++++
 vimrc                                     |   3 -
 13 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/__pycache__/format.cpython-39.pyc
 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/__pycache__/format.cpython-39.pyc b/pythonx/__pycache__/format.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b6b53dd68955cfe916d5b009316a111f21578def
GIT binary patch
literal 3001
zcmaJ@TW=f36`t8Um%7`Iqc#mTL7j>*NI4FW7J^a4u;oSwN(+_^0o$~j6=z5-z1(GH
zhmyrCU+Uzo`2$T0kdN(8Y5&2z_Q~jLo{S>NIkTiJ8Er|-{dVS@@B3!s<z=71@8AFY
z-RJk#2>BN_F8(ZNd<?1h95O~i!YDIR8d6;vp`lANG<9i(mM-nk{taO!v%VtC;?8q3
zbeYYZuSn=Imw8auSdIBm`mD~DpsX|FfHaoJjr}uuoM%Bd7lT;xXG4M2pp(W~uUT~l
z?*(V^u=@fGBJB?57k?BQA44kOHiQ!}l%SDdARb_<`UAc5t`NBxU%5L!NG4e?;Mmt{
zH!S5vQ5Fw)6e&N7200t0SYL{wFGg`XA916I<((+{8xbxXrL&hex1SVT6x&BUOZaxH
zlV{zex1F6NgYBWd*t6~G60PBxFko37O~r~wb^FqNYS54A9!~lWGD-U2_)Rh)(vV~d
zcbd`Xl$mdm2`wqJo)WRe?1ROqgQF9pH0HBiY+onLgZXA@d`noZG`}T(H7Azd*@vC&
zl3+;Kvb0Lvd!@A4YH9as69?wleW&k=KbH<$TeNz{FA0o!r2}WVrCa=6TTq+&rOT{M
zGNTi(^rm&TKBM9va*1s`r+=h!8Ad&3e{F1%=aj<DrYr19>9MPuVBdA}2CSn5cWPW4
zH}A!1dKm)%5qZpm&PWKJ$pDZdgZ!w^JF*q7YqUg_qoZLgkH?#7lJVO|XOeHd+j_SV
z+zdK-s;e*1`2tbXe(9X-q}gcj(mmPf<fBZ!!0wlHC)~KSV<FE{K6aYAR8}{EmfHkV
z3V@W;#G{JwYO@PQAH|)g)x7aqGcJnJfH!eIVklhO%R2dBm~zQ0P>vfHalR8I=i|Xv
zahA#WS%uM}(HgtGT;{<I7CQIz!HrdA0?d?k5{p8aNhX!WPvTUWg%rx2J4!i6qi&ZA
zWuXPi>VS>P>KA#Y>@<%Vtn6{Ak*HP$AEt4KM}qf~JS$+6d@zVJ)~Knaiw;pvb$3-O
zWGp1!KXiG<umannJHnNPHzMsd@fLViIr&HqM^ZV*JZ8YYa~=r~gYq-TRv{JdLq-hW
z+n^q`j1~F@U7^0|(UtEl%cmO#^y+?}dd8Ze*InWf?rTHRJQ7g(?>s^d&6#3WOqm$W
zngS0>V`fZDy~+lTSilhnc;S|2&z{)QE^WZRQ#!>Tm<Q~G8eqG&z&7?Z{P7KqVwA4n
z|Dm^rGc;-iO0z(P7`tEInl!)sU=sRzv_hcUoI?vL+RN=1Kftfy3iO&l0Tk<)Z9t}+
ziVxyC_WTsGhN~>xP1!nZ%7l=K0H;C+AY8{tyo1wU3(O7Z`v_8T6*346gk*y*Q7G*y
z6ar^-kUfhZx*vDZA@~ba7@bgb=+r<F6St%}wPsqzERY##_ML~sAQ9Hwl13hswUR*Y
zFA@lz`HhY8)~`dDmgI7OrN26((=}$#D09A{pFz+dSe7`E>!pRVQ<}w}<T`Vw8xTSd
z#i{C@UcoiftIV2F2v#kA3yjzC(kfHoJ^;d1PSo0in$z<A0DgBx515_FVn;WzYtEHm
zD@_X7*a<3z>vv``1luA1TIXxDifsX3Vg6%OG?;P3gCPh9XV@qtOR^yEE_R<E|A@<x
z$Sxd?XTlaBkQKvl30|bKUUCr>!!!ZK%z})c1{yrC%xLEs-wF1rCB^7yDDn<3il7^V
za<OV&TiK^c_Tg=KEFG{4j?pEM7|{b(s=7wiXqaT(Tv2~)k7T#`s|Hnek^$+*SK9f)
zx~|9v0U*4kb+~a2H0!KT9;lB*i<PgpDozu54E1w^!urAEyN@0}+23nFcnDt@rw`J&
zSA<t}?}O;c!S2B)4-XIb+V>wGhVH>(^vT}+-LQUoaGakWCK=05mA#)2d8VxWz5QKP
z$H0uTqyrxwAc}dCRYXFRi))Z6hl3)6Qt3D2=emb+?14+8LmPHQnkEjpbH53cFL4vI
z*LZ|f{1j609%N+60uFg}4MfaAL4&{XAIG-n#t*(>q4a^&Y5HDb7Vhal(#(1U6?jVF
zBS9K{vkyWwGl6GzNvF;X9@_il_pQ%>GsugH4Uz@yduoceOH$gUgMu|*XO`C2#vSs-
z+YI?&j!z#|kHI;FSTKJU;FY@c6ju+-|0ae8+c_-|qYG5BpjvwokL}Rr_|XMo<+7yP
zO_QT=^#Hmc68yKZ>>LB+wA{Ulk|+2He1vfp*5NUW@*zg309g#cJE05TmAFcoGVX;&
z@&TmVq0xu3fBW?ap-!fd3Ve`=vq66Xaj4XDNubxoo0#3g>_g1%V21Xn+U~OsA4+&R
tE4bimrgia^0$(^yBLfdn-ks`$^VcY!T461C;Pl_{%r(pVg?G(+=SPSV6E^?=

literal 0
HcmV?d00001

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 8c0dc1d..b30e37a 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