summaryrefslogtreecommitdiffstats
path: root/tools/clang-format
diff options
context:
space:
mode:
Diffstat (limited to 'tools/clang-format')
-rw-r--r--tools/clang-format/CMakeLists.txt10
-rw-r--r--tools/clang-format/ClangFormat.cpp202
-rw-r--r--tools/clang-format/Makefile2
-rwxr-xr-xtools/clang-format/clang-format-diff.py118
-rw-r--r--tools/clang-format/clang-format-sublime.py58
-rw-r--r--tools/clang-format/clang-format.el57
-rw-r--r--tools/clang-format/clang-format.py50
-rwxr-xr-xtools/clang-format/git-clang-format481
8 files changed, 821 insertions, 157 deletions
diff --git a/tools/clang-format/CMakeLists.txt b/tools/clang-format/CMakeLists.txt
index c86a920..7bb3fbf 100644
--- a/tools/clang-format/CMakeLists.txt
+++ b/tools/clang-format/CMakeLists.txt
@@ -12,6 +12,10 @@ target_link_libraries(clang-format
clangRewriteFrontend
)
-install(TARGETS clang-format
- RUNTIME DESTINATION bin)
-
+install(TARGETS clang-format RUNTIME DESTINATION bin)
+install(PROGRAMS clang-format-bbedit.applescript DESTINATION share/clang)
+install(PROGRAMS clang-format-diff.py DESTINATION share/clang)
+install(PROGRAMS clang-format-sublime.py DESTINATION share/clang)
+install(PROGRAMS clang-format.el DESTINATION share/clang)
+install(PROGRAMS clang-format.py DESTINATION share/clang)
+install(PROGRAMS git-clang-format DESTINATION bin)
diff --git a/tools/clang-format/ClangFormat.cpp b/tools/clang-format/ClangFormat.cpp
index 57833ed..768165b 100644
--- a/tools/clang-format/ClangFormat.cpp
+++ b/tools/clang-format/ClangFormat.cpp
@@ -20,32 +20,76 @@
#include "clang/Format/Format.h"
#include "clang/Lex/Lexer.h"
#include "clang/Rewrite/Core/Rewriter.h"
+#include "llvm/Support/Debug.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Signals.h"
+#include "llvm/ADT/StringMap.h"
using namespace llvm;
static cl::opt<bool> Help("h", cl::desc("Alias for -help"), cl::Hidden);
+// Mark all our options with this category, everything else (except for -version
+// and -help) will be hidden.
+cl::OptionCategory ClangFormatCategory("Clang-format options");
+
static cl::list<unsigned>
-Offsets("offset", cl::desc("Format a range starting at this file offset. Can "
- "only be used with one input file."));
+ Offsets("offset",
+ cl::desc("Format a range starting at this byte offset.\n"
+ "Multiple ranges can be formatted by specifying\n"
+ "several -offset and -length pairs.\n"
+ "Can only be used with one input file."),
+ cl::cat(ClangFormatCategory));
static cl::list<unsigned>
-Lengths("length", cl::desc("Format a range of this length. "
- "When it's not specified, end of file is used. "
- "Can only be used with one input file."));
-static cl::opt<std::string> Style(
- "style",
- cl::desc("Coding style, currently supports: LLVM, Google, Chromium, Mozilla."),
- cl::init("LLVM"));
+ Lengths("length",
+ cl::desc("Format a range of this length (in bytes).\n"
+ "Multiple ranges can be formatted by specifying\n"
+ "several -offset and -length pairs.\n"
+ "When only a single -offset is specified without\n"
+ "-length, clang-format will format up to the end\n"
+ "of the file.\n"
+ "Can only be used with one input file."),
+ cl::cat(ClangFormatCategory));
+static cl::list<std::string>
+LineRanges("lines", cl::desc("<start line>:<end line> - format a range of\n"
+ "lines (both 1-based).\n"
+ "Multiple ranges can be formatted by specifying\n"
+ "several -lines arguments.\n"
+ "Can't be used with -offset and -length.\n"
+ "Can only be used with one input file."),
+ cl::cat(ClangFormatCategory));
+static cl::opt<std::string>
+ Style("style",
+ cl::desc(clang::format::StyleOptionHelpDescription),
+ cl::init("file"), cl::cat(ClangFormatCategory));
+
+static cl::opt<std::string>
+AssumeFilename("assume-filename",
+ cl::desc("When reading from stdin, clang-format assumes this\n"
+ "filename to look for a style config file (with\n"
+ "-style=file)."),
+ cl::cat(ClangFormatCategory));
+
static cl::opt<bool> Inplace("i",
- cl::desc("Inplace edit <file>s, if specified."));
+ cl::desc("Inplace edit <file>s, if specified."),
+ cl::cat(ClangFormatCategory));
-static cl::opt<bool> OutputXML(
- "output-replacements-xml", cl::desc("Output replacements as XML."));
+static cl::opt<bool> OutputXML("output-replacements-xml",
+ cl::desc("Output replacements as XML."),
+ cl::cat(ClangFormatCategory));
+static cl::opt<bool>
+ DumpConfig("dump-config",
+ cl::desc("Dump configuration options to stdout and exit.\n"
+ "Can be used with -style option."),
+ cl::cat(ClangFormatCategory));
+static cl::opt<unsigned>
+ Cursor("cursor",
+ cl::desc("The position of the cursor when invoking\n"
+ "clang-format from an editor integration"),
+ cl::init(0), cl::cat(ClangFormatCategory));
-static cl::list<std::string> FileNames(cl::Positional,
- cl::desc("[<file> ...]"));
+static cl::list<std::string> FileNames(cl::Positional, cl::desc("[<file> ...]"),
+ cl::cat(ClangFormatCategory));
namespace clang {
namespace format {
@@ -59,34 +103,43 @@ static FileID createInMemoryFile(StringRef FileName, const MemoryBuffer *Source,
return Sources.createFileID(Entry, SourceLocation(), SrcMgr::C_User);
}
-static FormatStyle getStyle() {
- FormatStyle TheStyle = getGoogleStyle();
- if (Style == "LLVM")
- TheStyle = getLLVMStyle();
- else if (Style == "Chromium")
- TheStyle = getChromiumStyle();
- else if (Style == "Mozilla")
- TheStyle = getMozillaStyle();
- else if (Style != "Google")
- llvm::errs() << "Unknown style " << Style << ", using Google style.\n";
-
- return TheStyle;
+// Parses <start line>:<end line> input to a pair of line numbers.
+// Returns true on error.
+static bool parseLineRange(StringRef Input, unsigned &FromLine,
+ unsigned &ToLine) {
+ std::pair<StringRef, StringRef> LineRange = Input.split(':');
+ return LineRange.first.getAsInteger(0, FromLine) ||
+ LineRange.second.getAsInteger(0, ToLine);
}
-// Returns true on error.
-static bool format(std::string FileName) {
- FileManager Files((FileSystemOptions()));
- DiagnosticsEngine Diagnostics(
- IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs),
- new DiagnosticOptions);
- SourceManager Sources(Diagnostics, Files);
- OwningPtr<MemoryBuffer> Code;
- if (error_code ec = MemoryBuffer::getFileOrSTDIN(FileName, Code)) {
- llvm::errs() << ec.message() << "\n";
- return true;
+static bool fillRanges(SourceManager &Sources, FileID ID,
+ const MemoryBuffer *Code,
+ std::vector<CharSourceRange> &Ranges) {
+ if (!LineRanges.empty()) {
+ if (!Offsets.empty() || !Lengths.empty()) {
+ llvm::errs() << "error: cannot use -lines with -offset/-length\n";
+ return true;
+ }
+
+ for (unsigned i = 0, e = LineRanges.size(); i < e; ++i) {
+ unsigned FromLine, ToLine;
+ if (parseLineRange(LineRanges[i], FromLine, ToLine)) {
+ llvm::errs() << "error: invalid <start line>:<end line> pair\n";
+ return true;
+ }
+ if (FromLine > ToLine) {
+ llvm::errs() << "error: start line should be less than end line\n";
+ return true;
+ }
+ SourceLocation Start = Sources.translateLineCol(ID, FromLine, 1);
+ SourceLocation End = Sources.translateLineCol(ID, ToLine, UINT_MAX);
+ if (Start.isInvalid() || End.isInvalid())
+ return true;
+ Ranges.push_back(CharSourceRange::getCharRange(Start, End));
+ }
+ return false;
}
- FileID ID = createInMemoryFile(FileName, Code.get(), Sources, Files);
- Lexer Lex(ID, Sources.getBuffer(ID), Sources, getFormattingLangOpts());
+
if (Offsets.empty())
Offsets.push_back(0);
if (Offsets.size() != Lengths.size() &&
@@ -95,7 +148,6 @@ static bool format(std::string FileName) {
<< "error: number of -offset and -length arguments must match.\n";
return true;
}
- std::vector<CharSourceRange> Ranges;
for (unsigned i = 0, e = Offsets.size(); i != e; ++i) {
if (Offsets[i] >= Code->getBufferSize()) {
llvm::errs() << "error: offset " << Offsets[i]
@@ -118,7 +170,33 @@ static bool format(std::string FileName) {
}
Ranges.push_back(CharSourceRange::getCharRange(Start, End));
}
- tooling::Replacements Replaces = reformat(getStyle(), Lex, Sources, Ranges);
+ return false;
+}
+
+// Returns true on error.
+static bool format(StringRef FileName) {
+ FileManager Files((FileSystemOptions()));
+ DiagnosticsEngine Diagnostics(
+ IntrusiveRefCntPtr<DiagnosticIDs>(new DiagnosticIDs),
+ new DiagnosticOptions);
+ SourceManager Sources(Diagnostics, Files);
+ OwningPtr<MemoryBuffer> Code;
+ if (error_code ec = MemoryBuffer::getFileOrSTDIN(FileName, Code)) {
+ llvm::errs() << ec.message() << "\n";
+ return true;
+ }
+ if (Code->getBufferSize() == 0)
+ return false; // Empty files are formatted correctly.
+ FileID ID = createInMemoryFile(FileName, Code.get(), Sources, Files);
+ std::vector<CharSourceRange> Ranges;
+ if (fillRanges(Sources, ID, Code.get(), Ranges))
+ return true;
+
+ FormatStyle FormatStyle =
+ getStyle(Style, (FileName == "-") ? AssumeFilename : FileName);
+ Lexer Lex(ID, Sources.getBuffer(ID), Sources,
+ getFormattingLangOpts(FormatStyle.Standard));
+ tooling::Replacements Replaces = reformat(FormatStyle, Lex, Sources, Ranges);
if (OutputXML) {
llvm::outs()
<< "<?xml version='1.0'?>\n<replacements xml:space='preserve'>\n";
@@ -135,19 +213,12 @@ static bool format(std::string FileName) {
Rewriter Rewrite(Sources, LangOptions());
tooling::applyAllReplacements(Replaces, Rewrite);
if (Inplace) {
- if (Replaces.size() == 0)
- return false; // Nothing changed, don't touch the file.
-
- std::string ErrorInfo;
- llvm::raw_fd_ostream FileStream(FileName.c_str(), ErrorInfo,
- llvm::raw_fd_ostream::F_Binary);
- if (!ErrorInfo.empty()) {
- llvm::errs() << "Error while writing file: " << ErrorInfo << "\n";
+ if (Rewrite.overwriteChangedFiles())
return true;
- }
- Rewrite.getEditBuffer(ID).write(FileStream);
- FileStream.flush();
} else {
+ if (Cursor.getNumOccurrences() != 0)
+ outs() << "{ \"Cursor\": " << tooling::shiftedCodePosition(
+ Replaces, Cursor) << " }\n";
Rewrite.getEditBuffer(ID).write(outs());
}
}
@@ -159,18 +230,37 @@ static bool format(std::string FileName) {
int main(int argc, const char **argv) {
llvm::sys::PrintStackTraceOnErrorSignal();
+
+ // Hide unrelated options.
+ StringMap<cl::Option*> Options;
+ cl::getRegisteredOptions(Options);
+ for (StringMap<cl::Option *>::iterator I = Options.begin(), E = Options.end();
+ I != E; ++I) {
+ if (I->second->Category != &ClangFormatCategory && I->first() != "help" &&
+ I->first() != "version")
+ I->second->setHiddenFlag(cl::ReallyHidden);
+ }
+
cl::ParseCommandLineOptions(
argc, argv,
"A tool to format C/C++/Obj-C code.\n\n"
"If no arguments are specified, it formats the code from standard input\n"
"and writes the result to the standard output.\n"
- "If <file>s are given, it reformats the files. If -i is specified \n"
- "together with <file>s, the files are edited in-place. Otherwise, the \n"
+ "If <file>s are given, it reformats the files. If -i is specified\n"
+ "together with <file>s, the files are edited in-place. Otherwise, the\n"
"result is written to the standard output.\n");
if (Help)
cl::PrintHelpMessage();
+ if (DumpConfig) {
+ std::string Config =
+ clang::format::configurationAsText(clang::format::getStyle(
+ Style, FileNames.empty() ? AssumeFilename : FileNames[0]));
+ llvm::outs() << Config << "\n";
+ return 0;
+ }
+
bool Error = false;
switch (FileNames.size()) {
case 0:
@@ -180,8 +270,8 @@ int main(int argc, const char **argv) {
Error = clang::format::format(FileNames[0]);
break;
default:
- if (!Offsets.empty() || !Lengths.empty()) {
- llvm::errs() << "error: \"-offset\" and \"-length\" can only be used for "
+ if (!Offsets.empty() || !Lengths.empty() || !LineRanges.empty()) {
+ llvm::errs() << "error: -offset, -length and -lines can only be used for "
"single file.\n";
return 1;
}
diff --git a/tools/clang-format/Makefile b/tools/clang-format/Makefile
index d869267..4902244 100644
--- a/tools/clang-format/Makefile
+++ b/tools/clang-format/Makefile
@@ -15,7 +15,7 @@ TOOLNAME = clang-format
TOOL_NO_EXPORTS = 1
include $(CLANG_LEVEL)/../../Makefile.config
-LINK_COMPONENTS := $(TARGETS_TO_BUILD) asmparser bitreader support mc
+LINK_COMPONENTS := $(TARGETS_TO_BUILD) asmparser bitreader support mc option
USEDLIBS = clangFormat.a clangTooling.a clangFrontend.a clangSerialization.a \
clangDriver.a clangParse.a clangSema.a clangAnalysis.a \
clangRewriteFrontend.a clangRewriteCore.a clangEdit.a clangAST.a \
diff --git a/tools/clang-format/clang-format-diff.py b/tools/clang-format/clang-format-diff.py
index 68b5113..60b8fb7 100755
--- a/tools/clang-format/clang-format-diff.py
+++ b/tools/clang-format/clang-format-diff.py
@@ -17,13 +17,16 @@ This script reads input from a unified diff and reformats all the changed
lines. This is useful to reformat all the lines touched by a specific patch.
Example usage for git users:
- git diff -U0 HEAD^ | clang-format-diff.py -p1
+ git diff -U0 HEAD^ | clang-format-diff.py -p1 -i
"""
import argparse
+import difflib
import re
+import string
import subprocess
+import StringIO
import sys
@@ -31,67 +34,24 @@ import sys
binary = 'clang-format'
-def getOffsetLength(filename, line_number, line_count):
- """
- Calculates the field offset and length based on line number and count.
- """
- offset = 0
- length = 0
- with open(filename, 'r') as f:
- for line in f:
- if line_number > 1:
- offset += len(line)
- line_number -= 1
- elif line_count > 0:
- length += len(line)
- line_count -= 1
- else:
- break
- return offset, length
-
-
-def formatRange(r, style):
- """
- Formats range 'r' according to style 'style'.
- """
- filename, line_number, line_count = r
- # FIXME: Add other types containing C++/ObjC code.
- if not (filename.endswith(".cpp") or filename.endswith(".cc") or
- filename.endswith(".h")):
- return
-
- offset, length = getOffsetLength(filename, line_number, line_count)
- with open(filename, 'r') as f:
- text = f.read()
- command = [binary, '-offset', str(offset), '-length', str(length)]
- if style:
- command.extend(['-style', style])
- p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- stdin=subprocess.PIPE)
- stdout, stderr = p.communicate(input=text)
- if stderr:
- print stderr
- return
- if not stdout:
- print 'Segfault occurred while formatting', filename
- print 'Please report a bug on llvm.org/bugs.'
- return
- with open(filename, 'w') as f:
- f.write(stdout)
-
-
def main():
parser = argparse.ArgumentParser(description=
- 'Reformat changed lines in diff')
- parser.add_argument('-p', default=1,
+ 'Reformat changed lines in diff. Without -i '
+ 'option just output the diff that would be'
+ 'introduced.')
+ parser.add_argument('-i', action='store_true', default=False,
+ help='apply edits to files instead of displaying a diff')
+ parser.add_argument('-p', default=0,
help='strip the smallest prefix containing P slashes')
- parser.add_argument('-style',
- help='formatting style to apply (LLVM, Google, Chromium)')
+ parser.add_argument(
+ '-style',
+ help=
+ 'formatting style to apply (LLVM, Google, Chromium, Mozilla, WebKit)')
args = parser.parse_args()
+ # Extract changed lines for each file.
filename = None
- ranges = []
-
+ lines_by_file = {}
for line in sys.stdin:
match = re.search('^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line)
if match:
@@ -99,18 +59,50 @@ def main():
if filename == None:
continue
+ # FIXME: Add other types containing C++/ObjC code.
+ if not (filename.endswith(".cpp") or filename.endswith(".cc") or
+ filename.endswith(".h")):
+ continue
+
match = re.search('^@@.*\+(\d+)(,(\d+))?', line)
if match:
+ start_line = int(match.group(1))
line_count = 1
if match.group(3):
line_count = int(match.group(3))
- ranges.append((filename, int(match.group(1)), line_count))
-
- # Reverse the ranges so that the reformatting does not influence file offsets.
- for r in reversed(ranges):
- # Do the actual formatting.
- formatRange(r, args.style)
-
+ if line_count == 0:
+ continue
+ end_line = start_line + line_count - 1;
+ lines_by_file.setdefault(filename, []).extend(
+ ['-lines', str(start_line) + ':' + str(end_line)])
+
+ # Reformat files containing changes in place.
+ for filename, lines in lines_by_file.iteritems():
+ command = [binary, filename]
+ if args.i:
+ command.append('-i')
+ command.extend(lines)
+ if args.style:
+ command.extend(['-style', args.style])
+ p = subprocess.Popen(command, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ if stderr:
+ print stderr
+ if p.returncode != 0:
+ sys.exit(p.returncode);
+
+ if not args.i:
+ with open(filename) as f:
+ code = f.readlines()
+ formatted_code = StringIO.StringIO(stdout).readlines()
+ diff = difflib.unified_diff(code, formatted_code,
+ filename, filename,
+ '(before formatting)', '(after formatting)')
+ diff_string = string.join(diff, '')
+ if len(diff_string) > 0:
+ print diff_string
if __name__ == '__main__':
main()
diff --git a/tools/clang-format/clang-format-sublime.py b/tools/clang-format/clang-format-sublime.py
new file mode 100644
index 0000000..16ff56e
--- /dev/null
+++ b/tools/clang-format/clang-format-sublime.py
@@ -0,0 +1,58 @@
+# This file is a minimal clang-format sublime-integration. To install:
+# - Change 'binary' if clang-format is not on the path (see below).
+# - Put this file into your sublime Packages directory, e.g. on Linux:
+# ~/.config/sublime-text-2/Packages/User/clang-format-sublime.py
+# - Add a key binding:
+# { "keys": ["ctrl+shift+c"], "command": "clang_format" },
+#
+# With this integration you can press the bound key and clang-format will
+# format the current lines and selections for all cursor positions. The lines
+# or regions are extended to the next bigger syntactic entities.
+#
+# It operates on the current, potentially unsaved buffer and does not create
+# or save any files. To revert a formatting, just undo.
+
+from __future__ import print_function
+import sublime
+import sublime_plugin
+import subprocess
+
+# Change this to the full path if clang-format is not on the path.
+binary = 'clang-format'
+
+# Change this to format according to other formatting styles. See the output of
+# 'clang-format --help' for a list of supported styles. The default looks for
+# a '.clang-format' or '_clang-format' file to indicate the style that should be
+# used.
+style = 'file'
+
+class ClangFormatCommand(sublime_plugin.TextCommand):
+ def run(self, edit):
+ encoding = self.view.encoding()
+ if encoding == 'Undefined':
+ encoding = 'utf-8'
+ regions = []
+ command = [binary, '-style', style]
+ for region in self.view.sel():
+ regions.append(region)
+ region_offset = min(region.a, region.b)
+ region_length = abs(region.b - region.a)
+ command.extend(['-offset', str(region_offset),
+ '-length', str(region_length),
+ '-assume-filename', str(self.view.file_name())])
+ old_viewport_position = self.view.viewport_position()
+ buf = self.view.substr(sublime.Region(0, self.view.size()))
+ p = subprocess.Popen(command, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, stdin=subprocess.PIPE)
+ output, error = p.communicate(buf.encode(encoding))
+ if error:
+ print(error)
+ self.view.replace(
+ edit, sublime.Region(0, self.view.size()),
+ output.decode(encoding))
+ self.view.sel().clear()
+ for region in regions:
+ self.view.sel().add(region)
+ # FIXME: Without the 10ms delay, the viewport sometimes jumps.
+ sublime.set_timeout(lambda: self.view.set_viewport_position(
+ old_viewport_position, False), 10)
diff --git a/tools/clang-format/clang-format.el b/tools/clang-format/clang-format.el
index 2c5546b..520a3e2 100644
--- a/tools/clang-format/clang-format.el
+++ b/tools/clang-format/clang-format.el
@@ -7,25 +7,50 @@
;; (global-set-key [C-M-tab] 'clang-format-region)
;;
;; Depending on your configuration and coding style, you might need to modify
-;; 'style' and 'binary' below.
+;; 'style' in clang-format, below.
+
+(require 'json)
+
+;; *Location of the clang-format binary. If it is on your PATH, a full path name
+;; need not be specified.
+(defvar clang-format-binary "clang-format")
+
(defun clang-format-region ()
+ "Use clang-format to format the currently active region."
+ (interactive)
+ (let ((beg (if mark-active
+ (region-beginning)
+ (min (line-beginning-position) (1- (point-max)))))
+ (end (if mark-active
+ (region-end)
+ (line-end-position))))
+ (clang-format beg end)))
+
+(defun clang-format-buffer ()
+ "Use clang-format to format the current buffer."
(interactive)
+ (clang-format (point-min) (point-max)))
+(defun clang-format (begin end)
+ "Use clang-format to format the code between BEGIN and END."
(let* ((orig-windows (get-buffer-window-list (current-buffer)))
(orig-window-starts (mapcar #'window-start orig-windows))
(orig-point (point))
- (binary "clang-format")
- (style "LLVM"))
- (if mark-active
- (setq beg (region-beginning)
- end (region-end))
- (setq beg (min (line-beginning-position) (1- (point-max)))
- end (min (line-end-position) (1- (point-max)))))
- (call-process-region (point-min) (point-max) binary t t nil
- "-offset" (number-to-string (1- beg))
- "-length" (number-to-string (- end beg))
- "-style" style)
- (goto-char orig-point)
- (dotimes (index (length orig-windows))
- (set-window-start (nth index orig-windows)
- (nth index orig-window-starts)))))
+ (style "file"))
+ (unwind-protect
+ (call-process-region (point-min) (point-max) clang-format-binary
+ t (list t nil) nil
+ "-offset" (number-to-string (1- begin))
+ "-length" (number-to-string (- end begin))
+ "-cursor" (number-to-string (1- (point)))
+ "-assume-filename" (buffer-file-name)
+ "-style" style)
+ (goto-char (point-min))
+ (let ((json-output (json-read-from-string
+ (buffer-substring-no-properties
+ (point-min) (line-beginning-position 2)))))
+ (delete-region (point-min) (line-beginning-position 2))
+ (goto-char (1+ (cdr (assoc 'Cursor json-output))))
+ (dotimes (index (length orig-windows))
+ (set-window-start (nth index orig-windows)
+ (nth index orig-window-starts)))))))
diff --git a/tools/clang-format/clang-format.py b/tools/clang-format/clang-format.py
index d90c62a..f5a5756 100644
--- a/tools/clang-format/clang-format.py
+++ b/tools/clang-format/clang-format.py
@@ -17,31 +17,43 @@
# It operates on the current, potentially unsaved buffer and does not create
# or save any files. To revert a formatting, just undo.
-import vim
+import difflib
+import json
import subprocess
+import sys
+import vim
# Change this to the full path if clang-format is not on the path.
binary = 'clang-format'
-# Change this to format according to other formatting styles (see
-# clang-format -help)
-style = 'LLVM'
+# Change this to format according to other formatting styles. See the output of
+# 'clang-format --help' for a list of supported styles. The default looks for
+# a '.clang-format' or '_clang-format' file to indicate the style that should be
+# used.
+style = 'file'
# Get the current text.
buf = vim.current.buffer
-text = "\n".join(buf)
+text = '\n'.join(buf)
# Determine range to format.
-offset = int(vim.eval('line2byte(' +
- str(vim.current.range.start + 1) + ')')) - 1
-length = int(vim.eval('line2byte(' +
- str(vim.current.range.end + 2) + ')')) - offset - 2
+cursor = int(vim.eval('line2byte(line("."))+col(".")')) - 2
+lines = '%s:%s' % (vim.current.range.start + 1, vim.current.range.end + 1)
+
+# Avoid flashing an ugly, ugly cmd prompt on Windows when invoking clang-format.
+startupinfo = None
+if sys.platform.startswith('win32'):
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ startupinfo.wShowWindow = subprocess.SW_HIDE
# Call formatter.
-p = subprocess.Popen([binary, '-offset', str(offset), '-length', str(length),
- '-style', style],
+command = [binary, '-lines', lines, '-style', style, '-cursor', str(cursor)]
+if vim.current.buffer.name:
+ command.extend(['-assume-filename', vim.current.buffer.name])
+p = subprocess.Popen(command,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- stdin=subprocess.PIPE)
+ stdin=subprocess.PIPE, startupinfo=startupinfo)
stdout, stderr = p.communicate(input=text)
# If successful, replace buffer contents.
@@ -56,10 +68,12 @@ if stderr:
if not stdout:
print ('No output from clang-format (crashed?).\n' +
'Please report to bugs.llvm.org.')
-elif stdout != text:
+else:
lines = stdout.split('\n')
- for i in range(min(len(buf), len(lines))):
- buf[i] = lines[i]
- for line in lines[len(buf):]:
- buf.append(line)
- del buf[len(lines):]
+ output = json.loads(lines[0])
+ lines = lines[1:]
+ sequence = difflib.SequenceMatcher(None, vim.current.buffer, lines)
+ for op in reversed(sequence.get_opcodes()):
+ if op[0] is not 'equal':
+ vim.current.buffer[op[1]:op[2]] = lines[op[3]:op[4]]
+ vim.command('goto %d' % (output['Cursor'] + 1))
diff --git a/tools/clang-format/git-clang-format b/tools/clang-format/git-clang-format
new file mode 100755
index 0000000..b0737ed
--- /dev/null
+++ b/tools/clang-format/git-clang-format
@@ -0,0 +1,481 @@
+#!/usr/bin/python
+#
+#===- git-clang-format - ClangFormat Git Integration ---------*- python -*--===#
+#
+# The LLVM Compiler Infrastructure
+#
+# This file is distributed under the University of Illinois Open Source
+# License. See LICENSE.TXT for details.
+#
+#===------------------------------------------------------------------------===#
+
+r"""
+clang-format git integration
+============================
+
+This file provides a clang-format integration for git. Put it somewhere in your
+path and ensure that it is executable. Then, "git clang-format" will invoke
+clang-format on the changes in current files or a specific commit.
+
+For further details, run:
+git clang-format -h
+
+Requires Python 2.7
+"""
+
+import argparse
+import collections
+import contextlib
+import errno
+import os
+import re
+import subprocess
+import sys
+
+usage = 'git clang-format [OPTIONS] [<commit>] [--] [<file>...]'
+
+desc = '''
+Run clang-format on all lines that differ between the working directory
+and <commit>, which defaults to HEAD. Changes are only applied to the working
+directory.
+
+The following git-config settings set the default of the corresponding option:
+ clangFormat.binary
+ clangFormat.commit
+ clangFormat.extension
+ clangFormat.style
+'''
+
+# Name of the temporary index file in which save the output of clang-format.
+# This file is created within the .git directory.
+temp_index_basename = 'clang-format-index'
+
+
+Range = collections.namedtuple('Range', 'start, count')
+
+
+def main():
+ config = load_git_config()
+
+ # In order to keep '--' yet allow options after positionals, we need to
+ # check for '--' ourselves. (Setting nargs='*' throws away the '--', while
+ # nargs=argparse.REMAINDER disallows options after positionals.)
+ argv = sys.argv[1:]
+ try:
+ idx = argv.index('--')
+ except ValueError:
+ dash_dash = []
+ else:
+ dash_dash = argv[idx:]
+ argv = argv[:idx]
+
+ default_extensions = ','.join([
+ # From clang/lib/Frontend/FrontendOptions.cpp, all lower case
+ 'c', 'h', # C
+ 'm', # ObjC
+ 'mm', # ObjC++
+ 'cc', 'cp', 'cpp', 'c++', 'cxx', 'hpp', # C++
+ ])
+
+ p = argparse.ArgumentParser(
+ usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=desc)
+ p.add_argument('--binary',
+ default=config.get('clangformat.binary', 'clang-format'),
+ help='path to clang-format'),
+ p.add_argument('--commit',
+ default=config.get('clangformat.commit', 'HEAD'),
+ help='default commit to use if none is specified'),
+ p.add_argument('--diff', action='store_true',
+ help='print a diff instead of applying the changes')
+ p.add_argument('--extensions',
+ default=config.get('clangformat.extensions',
+ default_extensions),
+ help=('comma-separated list of file extensions to format, '
+ 'excluding the period and case-insensitive')),
+ p.add_argument('-f', '--force', action='store_true',
+ help='allow changes to unstaged files')
+ p.add_argument('-p', '--patch', action='store_true',
+ help='select hunks interactively')
+ p.add_argument('-q', '--quiet', action='count', default=0,
+ help='print less information')
+ p.add_argument('--style',
+ default=config.get('clangformat.style', None),
+ help='passed to clang-format'),
+ p.add_argument('-v', '--verbose', action='count', default=0,
+ help='print extra information')
+ # We gather all the remaining positional arguments into 'args' since we need
+ # to use some heuristics to determine whether or not <commit> was present.
+ # However, to print pretty messages, we make use of metavar and help.
+ p.add_argument('args', nargs='*', metavar='<commit>',
+ help='revision from which to compute the diff')
+ p.add_argument('ignored', nargs='*', metavar='<file>...',
+ help='if specified, only consider differences in these files')
+ opts = p.parse_args(argv)
+
+ opts.verbose -= opts.quiet
+ del opts.quiet
+
+ commit, files = interpret_args(opts.args, dash_dash, opts.commit)
+ changed_lines = compute_diff_and_extract_lines(commit, files)
+ if opts.verbose >= 1:
+ ignored_files = set(changed_lines)
+ filter_by_extension(changed_lines, opts.extensions.lower().split(','))
+ if opts.verbose >= 1:
+ ignored_files.difference_update(changed_lines)
+ if ignored_files:
+ print 'Ignoring changes in the following files (wrong extension):'
+ for filename in ignored_files:
+ print ' ', filename
+ if changed_lines:
+ print 'Running clang-format on the following files:'
+ for filename in changed_lines:
+ print ' ', filename
+ if not changed_lines:
+ print 'no modified files to format'
+ return
+ # The computed diff outputs absolute paths, so we must cd before accessing
+ # those files.
+ cd_to_toplevel()
+ old_tree = create_tree_from_workdir(changed_lines)
+ new_tree = run_clang_format_and_save_to_tree(changed_lines,
+ binary=opts.binary,
+ style=opts.style)
+ if opts.verbose >= 1:
+ print 'old tree:', old_tree
+ print 'new tree:', new_tree
+ if old_tree == new_tree:
+ if opts.verbose >= 0:
+ print 'clang-format did not modify any files'
+ elif opts.diff:
+ print_diff(old_tree, new_tree)
+ else:
+ changed_files = apply_changes(old_tree, new_tree, force=opts.force,
+ patch_mode=opts.patch)
+ if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1:
+ print 'changed files:'
+ for filename in changed_files:
+ print ' ', filename
+
+
+def load_git_config(non_string_options=None):
+ """Return the git configuration as a dictionary.
+
+ All options are assumed to be strings unless in `non_string_options`, in which
+ is a dictionary mapping option name (in lower case) to either "--bool" or
+ "--int"."""
+ if non_string_options is None:
+ non_string_options = {}
+ out = {}
+ for entry in run('git', 'config', '--list', '--null').split('\0'):
+ if entry:
+ name, value = entry.split('\n', 1)
+ if name in non_string_options:
+ value = run('git', 'config', non_string_options[name], name)
+ out[name] = value
+ return out
+
+
+def interpret_args(args, dash_dash, default_commit):
+ """Interpret `args` as "[commit] [--] [files...]" and return (commit, files).
+
+ It is assumed that "--" and everything that follows has been removed from
+ args and placed in `dash_dash`.
+
+ If "--" is present (i.e., `dash_dash` is non-empty), the argument to its
+ left (if present) is taken as commit. Otherwise, the first argument is
+ checked if it is a commit or a file. If commit is not given,
+ `default_commit` is used."""
+ if dash_dash:
+ if len(args) == 0:
+ commit = default_commit
+ elif len(args) > 1:
+ die('at most one commit allowed; %d given' % len(args))
+ else:
+ commit = args[0]
+ object_type = get_object_type(commit)
+ if object_type not in ('commit', 'tag'):
+ if object_type is None:
+ die("'%s' is not a commit" % commit)
+ else:
+ die("'%s' is a %s, but a commit was expected" % (commit, object_type))
+ files = dash_dash[1:]
+ elif args:
+ if disambiguate_revision(args[0]):
+ commit = args[0]
+ files = args[1:]
+ else:
+ commit = default_commit
+ files = args
+ else:
+ commit = default_commit
+ files = []
+ return commit, files
+
+
+def disambiguate_revision(value):
+ """Returns True if `value` is a revision, False if it is a file, or dies."""
+ # If `value` is ambiguous (neither a commit nor a file), the following
+ # command will die with an appropriate error message.
+ run('git', 'rev-parse', value, verbose=False)
+ object_type = get_object_type(value)
+ if object_type is None:
+ return False
+ if object_type in ('commit', 'tag'):
+ return True
+ die('`%s` is a %s, but a commit or filename was expected' %
+ (value, object_type))
+
+
+def get_object_type(value):
+ """Returns a string description of an object's type, or None if it is not
+ a valid git object."""
+ cmd = ['git', 'cat-file', '-t', value]
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ if p.returncode != 0:
+ return None
+ return stdout.strip()
+
+
+def compute_diff_and_extract_lines(commit, files):
+ """Calls compute_diff() followed by extract_lines()."""
+ diff_process = compute_diff(commit, files)
+ changed_lines = extract_lines(diff_process.stdout)
+ diff_process.stdout.close()
+ diff_process.wait()
+ if diff_process.returncode != 0:
+ # Assume error was already printed to stderr.
+ sys.exit(2)
+ return changed_lines
+
+
+def compute_diff(commit, files):
+ """Return a subprocess object producing the diff from `commit`.
+
+ The return value's `stdin` file object will produce a patch with the
+ differences between the working directory and `commit`, filtered on `files`
+ (if non-empty). Zero context lines are used in the patch."""
+ cmd = ['git', 'diff-index', '-p', '-U0', commit, '--']
+ cmd.extend(files)
+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ p.stdin.close()
+ return p
+
+
+def extract_lines(patch_file):
+ """Extract the changed lines in `patch_file`.
+
+ The return value is a dictionary mapping filename to a list of (start_line,
+ line_count) pairs.
+
+ The input must have been produced with ``-U0``, meaning unidiff format with
+ zero lines of context. The return value is a dict mapping filename to a
+ list of line `Range`s."""
+ matches = {}
+ for line in patch_file:
+ match = re.search(r'^\+\+\+\ [^/]+/(.*)', line)
+ if match:
+ filename = match.group(1).rstrip('\r\n')
+ match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line)
+ if match:
+ start_line = int(match.group(1))
+ line_count = 1
+ if match.group(3):
+ line_count = int(match.group(3))
+ if line_count > 0:
+ matches.setdefault(filename, []).append(Range(start_line, line_count))
+ return matches
+
+
+def filter_by_extension(dictionary, allowed_extensions):
+ """Delete every key in `dictionary` that doesn't have an allowed extension.
+
+ `allowed_extensions` must be a collection of lowercase file extensions,
+ excluding the period."""
+ allowed_extensions = frozenset(allowed_extensions)
+ for filename in dictionary.keys():
+ base_ext = filename.rsplit('.', 1)
+ if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions:
+ del dictionary[filename]
+
+
+def cd_to_toplevel():
+ """Change to the top level of the git repository."""
+ toplevel = run('git', 'rev-parse', '--show-toplevel')
+ os.chdir(toplevel)
+
+
+def create_tree_from_workdir(filenames):
+ """Create a new git tree with the given files from the working directory.
+
+ Returns the object ID (SHA-1) of the created tree."""
+ return create_tree(filenames, '--stdin')
+
+
+def run_clang_format_and_save_to_tree(changed_lines, binary='clang-format',
+ style=None):
+ """Run clang-format on each file and save the result to a git tree.
+
+ Returns the object ID (SHA-1) of the created tree."""
+ def index_info_generator():
+ for filename, line_ranges in changed_lines.iteritems():
+ mode = oct(os.stat(filename).st_mode)
+ blob_id = clang_format_to_blob(filename, line_ranges, binary=binary,
+ style=style)
+ yield '%s %s\t%s' % (mode, blob_id, filename)
+ return create_tree(index_info_generator(), '--index-info')
+
+
+def create_tree(input_lines, mode):
+ """Create a tree object from the given input.
+
+ If mode is '--stdin', it must be a list of filenames. If mode is
+ '--index-info' is must be a list of values suitable for "git update-index
+ --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>". Any other mode
+ is invalid."""
+ assert mode in ('--stdin', '--index-info')
+ cmd = ['git', 'update-index', '--add', '-z', mode]
+ with temporary_index_file():
+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
+ for line in input_lines:
+ p.stdin.write('%s\0' % line)
+ p.stdin.close()
+ if p.wait() != 0:
+ die('`%s` failed' % ' '.join(cmd))
+ tree_id = run('git', 'write-tree')
+ return tree_id
+
+
+def clang_format_to_blob(filename, line_ranges, binary='clang-format',
+ style=None):
+ """Run clang-format on the given file and save the result to a git blob.
+
+ Returns the object ID (SHA-1) of the created blob."""
+ clang_format_cmd = [binary, filename]
+ if style:
+ clang_format_cmd.extend(['-style='+style])
+ clang_format_cmd.extend([
+ '-lines=%s:%s' % (start_line, start_line+line_count-1)
+ for start_line, line_count in line_ranges])
+ try:
+ clang_format = subprocess.Popen(clang_format_cmd, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ except OSError as e:
+ if e.errno == errno.ENOENT:
+ die('cannot find executable "%s"' % binary)
+ else:
+ raise
+ clang_format.stdin.close()
+ hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin']
+ hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout,
+ stdout=subprocess.PIPE)
+ clang_format.stdout.close()
+ stdout = hash_object.communicate()[0]
+ if hash_object.returncode != 0:
+ die('`%s` failed' % ' '.join(hash_object_cmd))
+ if clang_format.wait() != 0:
+ die('`%s` failed' % ' '.join(clang_format_cmd))
+ return stdout.rstrip('\r\n')
+
+
+@contextlib.contextmanager
+def temporary_index_file(tree=None):
+ """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting
+ the file afterward."""
+ index_path = create_temporary_index(tree)
+ old_index_path = os.environ.get('GIT_INDEX_FILE')
+ os.environ['GIT_INDEX_FILE'] = index_path
+ try:
+ yield
+ finally:
+ if old_index_path is None:
+ del os.environ['GIT_INDEX_FILE']
+ else:
+ os.environ['GIT_INDEX_FILE'] = old_index_path
+ os.remove(index_path)
+
+
+def create_temporary_index(tree=None):
+ """Create a temporary index file and return the created file's path.
+
+ If `tree` is not None, use that as the tree to read in. Otherwise, an
+ empty index is created."""
+ gitdir = run('git', 'rev-parse', '--git-dir')
+ path = os.path.join(gitdir, temp_index_basename)
+ if tree is None:
+ tree = '--empty'
+ run('git', 'read-tree', '--index-output='+path, tree)
+ return path
+
+
+def print_diff(old_tree, new_tree):
+ """Print the diff between the two trees to stdout."""
+ # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output
+ # is expected to be viewed by the user, and only the former does nice things
+ # like color and pagination.
+ subprocess.check_call(['git', 'diff', old_tree, new_tree, '--'])
+
+
+def apply_changes(old_tree, new_tree, force=False, patch_mode=False):
+ """Apply the changes in `new_tree` to the working directory.
+
+ Bails if there are local changes in those files and not `force`. If
+ `patch_mode`, runs `git checkout --patch` to select hunks interactively."""
+ changed_files = run('git', 'diff-tree', '-r', '-z', '--name-only', old_tree,
+ new_tree).rstrip('\0').split('\0')
+ if not force:
+ unstaged_files = run('git', 'diff-files', '--name-status', *changed_files)
+ if unstaged_files:
+ print >>sys.stderr, ('The following files would be modified but '
+ 'have unstaged changes:')
+ print >>sys.stderr, unstaged_files
+ print >>sys.stderr, 'Please commit, stage, or stash them first.'
+ sys.exit(2)
+ if patch_mode:
+ # In patch mode, we could just as well create an index from the new tree
+ # and checkout from that, but then the user will be presented with a
+ # message saying "Discard ... from worktree". Instead, we use the old
+ # tree as the index and checkout from new_tree, which gives the slightly
+ # better message, "Apply ... to index and worktree". This is not quite
+ # right, since it won't be applied to the user's index, but oh well.
+ with temporary_index_file(old_tree):
+ subprocess.check_call(['git', 'checkout', '--patch', new_tree])
+ index_tree = old_tree
+ else:
+ with temporary_index_file(new_tree):
+ run('git', 'checkout-index', '-a', '-f')
+ return changed_files
+
+
+def run(*args, **kwargs):
+ stdin = kwargs.pop('stdin', '')
+ verbose = kwargs.pop('verbose', True)
+ strip = kwargs.pop('strip', True)
+ for name in kwargs:
+ raise TypeError("run() got an unexpected keyword argument '%s'" % name)
+ p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ stdin=subprocess.PIPE)
+ stdout, stderr = p.communicate(input=stdin)
+ if p.returncode == 0:
+ if stderr:
+ if verbose:
+ print >>sys.stderr, '`%s` printed to stderr:' % ' '.join(args)
+ print >>sys.stderr, stderr.rstrip()
+ if strip:
+ stdout = stdout.rstrip('\r\n')
+ return stdout
+ if verbose:
+ print >>sys.stderr, '`%s` returned %s' % (' '.join(args), p.returncode)
+ if stderr:
+ print >>sys.stderr, stderr.rstrip()
+ sys.exit(2)
+
+
+def die(message):
+ print >>sys.stderr, 'error:', message
+ sys.exit(2)
+
+
+if __name__ == '__main__':
+ main()
OpenPOWER on IntegriCloud