Overhaul clang-include-fixer.el

General overhaul to fix many coding bugs, simplify the code, and improve readability.

* Clarify documentation strings of user options.
* Say that clang-include-fixer-executable is a file to have auto completion.
* Allow user to select available options for clang-include-fixer-input-format. Turn it into a symbol as it's not a free-form string.
* Remove clang-include-fixer-query-mode. This option was apparently used to select between two different operation modes, which is not a typical use case for user options. Provide two separate commands instead.
* Add a face for the overlay highlighting so that users can customize it.
Move user commands to the front so that readers of the code aren't buried in internal functions.
* Make process calls asynchronous. This is possible here because clang-include-fixer doesn't change files in place. This means input is no longer blocked while clang-include-fixer is running.
* Factor out logic in helper functions to keep functions short.
* Add comments where appropriate.
* Provide an alternative buffer replacement strategy for the case that a single line was inserted (the normal case in the case of clang-include-fixer). This keeps point, markers, and other buffer information intact.
* Use let-alist and association lists instead of property lists to shorten the code.
* Instead of highlighting only the first occurrence of a symbol, highlight all occurrences and move point to the closest one.
* Detect qualified names at point.
* Use filepos-to-bufferpos if available.
* Formatting.

Patch by Philipp Stephani!

llvm-svn: 283306
This commit is contained in:
Haojian Wu 2016-10-05 10:04:13 +00:00
parent cf43f179b1
commit 0e1a50e9c4
3 changed files with 415 additions and 206 deletions

View File

@ -121,26 +121,19 @@ in your ``.emacs``:
(add-to-list 'load-path "path/to/llvm/source/tools/clang/tools/extra/include-fixer/tool/"
(require 'clang-include-fixer)
Within Emacs the tool can be invoked with the command ``M-x clang-include-fixer``.
Within Emacs the tool can be invoked with the command
``M-x clang-include-fixer``. This will insert the header that defines the
first undefined symbol; if there is more than one header that would define the
symbol, the user is prompted to select one.
To include the header that defines the symbol at point, run
``M-x clang-include-fixer-at-point``.
Make sure Emacs can find :program:`clang-include-fixer`:
- Add the path to :program:`clang-include-fixer` to the PATH environment variable.
Customized settings in `.emacs`:
- ``(custom-set-variables '(clang-include-fixer-executable "/path/to/include-fixer"))``
Set clang-include-fixer binary file path.
- ``(custom-set-variables '(clang-include-fixer-query-mode t))``
Set to `t` if you want to insert ``#include`` for the symbol under the cursor.
Default is `nil`. Compared to normal mode, this mode won't parse the source
file and only search the sysmbol from database, which is faster than normal
mode.
See ``clang-include-fixer.el`` for more details.
- Either add the parent directory of :program:`clang-include-fixer` to the PATH
environment variable, or customize the Emacs user option
``clang-include-fixer-executable`` to point to the file name of the program.
How it Works
============

View File

@ -0,0 +1,40 @@
;;; clang-include-fixer-test.el --- unit tests for clang-include-fixer.el -*- lexical-binding: t; -*-
;;; Commentary:
;; Unit tests for clang-include-fixer.el.
;;; Code:
(require 'cc-mode)
(require 'ert)
(ert-deftest clang-include-fixer--insert-line ()
"Unit test for `clang-include-fixer--insert-line'."
(with-temp-buffer
(insert "aa\nab\nac\nad\n")
(let ((from (current-buffer)))
(with-temp-buffer
(insert "aa\nac\nad\n")
(let ((to (current-buffer)))
(should (clang-include-fixer--insert-line from to))
(should (equal (buffer-string) "aa\nab\nac\nad\n")))))
(should (equal (buffer-string) "aa\nab\nac\nad\n"))))
(ert-deftest clang-include-fixer--symbol-at-point ()
"Unit test for `clang-include-fixer--symbol-at-point'."
(with-temp-buffer
(insert "a+bbb::cc")
(c++-mode)
(goto-char (point-min))
(should (equal (clang-include-fixer--symbol-at-point) "a"))
(forward-char)
;; Emacs treats the character immediately following a symbol as part of the
;; symbol.
(should (equal (clang-include-fixer--symbol-at-point) "a"))
(forward-char)
(should (equal (clang-include-fixer--symbol-at-point) "bbb::cc"))
(goto-char (point-max))
(should (equal (clang-include-fixer--symbol-at-point) "bbb::cc"))))
;;; clang-include-fixer-test.el ends here

View File

@ -1,7 +1,7 @@
;;; clang-include-fxier.el --- Emacs integration of the clang include fixer
;;; clang-include-fixer.el --- Emacs integration of the clang include fixer -*- lexical-binding: t; -*-
;; Keywords: tools, c
;; Package-Requires: ((json "1.2"))
;; Package-Requires: ((cl-lib "0.5") (json "1.2") (let-alist "1.0.4"))
;;; Commentary:
@ -12,219 +12,395 @@
;;; Code:
(require 'cl-lib)
(require 'json)
(require 'let-alist)
(defgroup clang-include-fixer nil
"Include fixer."
"Clang-based include fixer."
:group 'tools)
(defcustom clang-include-fixer-executable
"clang-include-fixer"
"Location of the `clang-include-fixer' executable.
"Location of the clang-include-fixer executable.
A string containing the name or the full path of the executable."
A string containing the name or the full path of the executable."
:group 'clang-include-fixer
:type 'string
:type '(file :must-match t)
:risky t)
(defcustom clang-include-fixer-input-format
"yaml"
"clang-include-fixer input format."
'yaml
"Input format for clang-include-fixer.
This string is passed as -db argument to
`clang-include-fixer-executable'."
:group 'clang-include-fixer
:type 'string
:type '(radio
(const :tag "Hard-coded mapping" :fixed)
(const :tag "YAML" yaml)
(symbol :tag "Other"))
:risky t)
(defcustom clang-include-fixer-init-string
""
"clang-include-fixer init string."
"Database initialization string for clang-include-fixer.
This string is passed as -input argument to
`clang-include-fixer-executable'."
:group 'clang-include-fixer
:type 'string
:risky t)
(defcustom clang-include-fixer-query-mode
nil
"clang-include-fixer query mode."
:group 'clang-include-fixer
:type 'boolean
:risky t)
(defun clang-include-fixer-call-executable (callee
include-fixer-parameter-a
&optional include-fixer-parameter-b
&optional include-fixer-parameter-c
)
"Calls clang-include-fixer with parameters INCLUDE-FIXER-PARAMETER-[ABC].
If the call was successful the returned result is stored in a temp buffer
and the function CALLEE is called on this temp buffer."
(let ((temp-buffer (generate-new-buffer " *clang-include-fixer-temp*"))
(temp-file (make-temp-file "clang-include-fixer")))
(unwind-protect
(let (status stderr operations)
(if (eq include-fixer-parameter-c nil)
(setq status
(call-process-region
(point-min) (point-max) clang-include-fixer-executable
nil `(,temp-buffer ,temp-file) nil
"-stdin"
include-fixer-parameter-a
(buffer-file-name)
))
(setq status
(call-process-region
(point-min) (point-max) clang-include-fixer-executable
nil `(,temp-buffer ,temp-file) nil
"-stdin"
include-fixer-parameter-a
include-fixer-parameter-b
include-fixer-parameter-c
(buffer-file-name)
)))
(setq stderr
(with-temp-buffer
(insert-file-contents temp-file)
(when (> (point-max) (point-min))
(insert ": "))
(buffer-substring-no-properties
(point-min) (line-end-position))))
(cond
((stringp status)
(error "(clang-include-fixer killed by signal %s%s)" status
stderr))
((not (equal 0 status))
(error "(clang-include-fixer failed with code %d%s)" status
stderr)))
(funcall callee temp-buffer))
(delete-file temp-file)
(when (buffer-name temp-buffer) (kill-buffer temp-buffer)))))
(defun clang-include-fixer-replace_buffer (temp-buffer)
"Replace current buffer by content of TEMP-BUFFER"
(with-current-buffer temp-buffer
(setq temp-start (point-min))
(setq temp-end (point-max))
)
(barf-if-buffer-read-only)
(erase-buffer)
(save-excursion
(insert-buffer-substring temp-buffer temp-start temp-end)))
(defun clang-include-fixer-add-header (temp-buffer)
"Analyse the result of include-fixer stored in TEMP_BUFFER and add a
missing header if there is any. If there are multiple possible headers
the user can select one of them to be included."
(with-current-buffer temp-buffer
(setq result (buffer-substring (point-min) (point-max)))
(setq include-fixer-context
(let ((json-object-type 'plist))
(json-read-from-string result))))
;; The header-infos is already sorted by include-fixer.
(setq header-infos (plist-get include-fixer-context :HeaderInfos))
(setq query-symbol-infos (plist-get include-fixer-context :QuerySymbolInfos))
(if (eq 0 (length query-symbol-infos))
(message "The file is fine, no need to add a header.")
(setq symbol-info (elt query-symbol-infos 0))
(setq symbol (plist-get symbol-info :RawIdentifier))
(setq symbol-offset (plist-get (plist-get symbol-info :Range)
:Offset))
;; Check the number of choices
(if (eq 0 (length header-infos))
(progn
(goto-char (1+ symbol-offset))
(message (concat "Couldn't find header for '" symbol "'.")))
(setq symbol-length (plist-get (plist-get symbol-info :Range)
:Length))
(goto-char (1+ symbol-offset))
(setq symbol-overlay (make-overlay (1+ symbol-offset)
(+ symbol-offset symbol-length +1)))
(overlay-put symbol-overlay 'face '(:background "green" :foreground
"black"))
(message (number-to-string symbol-offset))
(message (number-to-string symbol-length))
(if (eq 1 (length header-infos))
(progn
(setq missing-header
(plist-get (elt header-infos 0) :Header))
(message (concat "Only one include is missing: "
missing-header )))
;; Now iterate over vector and add items to list
(setq include-list '())
(setq index 0)
(while (< index (length header-infos))
(setq entry (elt header-infos index))
(add-to-list 'include-list (plist-get entry :Header))
(setq index (1+ index))
)
(setq option-message (concat "Select include for '"
symbol
"' :"))
(setq missing-header (ido-completing-read
option-message include-list)))
;; Now select set correct header info.
(setq header-plist '())
(setq index 0)
(while (< index (length header-infos))
(setq entry (elt header-infos index))
(setq index (1+ index))
(if (eq (plist-get entry :Header) missing-header)
(setq header-plist entry)))
(setq include-fixer-context (plist-put
include-fixer-context
':HeaderInfos (vector header-plist)))
(clang-include-fixer-call-executable
'clang-include-fixer-replace_buffer
(concat "-insert-header=" (json-encode include-fixer-context)))
(delete-overlay symbol-overlay))))
(defface clang-include-fixer-highlight '((t :background "green"))
"Used for highlighting the symbol for which a header file is being added.")
(defun clang-include-fixer ()
"Invokes the Include Fixer to insert missing C++ headers."
"Invoke the Include Fixer to insert missing C++ headers."
(interactive)
(message (concat "Calling the include fixer. "
"This might take some seconds. Please wait."))
(if clang-include-fixer-query-mode
(let (p1 p2)
(save-excursion
(skip-chars-backward "-a-zA-Z0-9_:")
(setq p1 (point))
(skip-chars-forward "-a-zA-Z0-9_:")
(setq p2 (point))
(setq query-symbol (buffer-substring-no-properties p1 p2))
(if (string= "" query-symbol)
(message "Skip querying empty symbol.")
(clang-include-fixer-call-executable
'clang-include-fixer-add-header
(concat "-db=" clang-include-fixer-input-format)
(concat "-input=" clang-include-fixer-init-string)
(concat "-query-symbol=" (thing-at-point 'symbol))
))))
(clang-include-fixer-call-executable
'clang-include-fixer-add-header
(concat "-db=" clang-include-fixer-input-format)
(concat "-input=" clang-include-fixer-init-string)
(clang-include-fixer--start #'clang-include-fixer--add-header
"-output-headers"))
)
(defun clang-include-fixer-at-point ()
"Invoke the Clang include fixer for the symbol at point."
(interactive)
(let ((symbol (clang-include-fixer--symbol-at-point)))
(unless symbol
(user-error "No symbol at current location"))
(clang-include-fixer--start #'clang-include-fixer--add-header
(format "-query-symbol=%s" symbol))))
(defun clang-include-fixer--start (callback &rest args)
"Asynchronously start clang-include-fixer with parameters ARGS.
The current file name is passed after ARGS as last argument. If
the call was successful the returned result is stored in a
temporary buffer, and CALLBACK is called with the temporary
buffer as only argument."
(let ((process (if (fboundp 'make-process)
;; Prefer using make-process if available, because
;; start-process doesnt allow us to separate the
;; standard error from the output.
(clang-include-fixer--make-process callback args)
(clang-include-fixer--start-process callback args))))
(save-restriction
(widen)
(process-send-region process (point-min) (point-max)))
(process-send-eof process))
nil)
(defun clang-include-fixer--make-process (callback args)
"Start a new clang-incude-fixer process using `make-process'.
CALLBACK is called after the process finishes successfully; it is
called with a single argument, the buffer where standard output
has been inserted. ARGS is a list of additional command line
arguments. Return the new process object."
(let* ((stdin (current-buffer))
(stdout (generate-new-buffer "*clang-include-fixer output*"))
(stderr (generate-new-buffer "*clang-include-fixer errors*")))
(make-process :name "clang-include-fixer"
:buffer stdout
:command (clang-include-fixer--command args)
:coding 'utf-8-unix
:connection-type 'pipe
:sentinel (clang-include-fixer--sentinel stdin stdout stderr
callback)
:stderr stderr)))
(defun clang-include-fixer--start-process (callback args)
"Start a new clang-incude-fixer process using `start-process'.
CALLBACK is called after the process finishes successfully; it is
called with a single argument, the buffer where standard output
has been inserted. ARGS is a list of additional command line
arguments. Return the new process object."
(let* ((stdin (current-buffer))
(stdout (generate-new-buffer "*clang-include-fixer output*"))
(process-connection-type nil)
(process (apply #'start-process "clang-include-fixer" stdout
(clang-include-fixer--command args))))
(set-process-coding-system process 'utf-8-unix 'utf-8-unix)
(set-process-sentinel process
(clang-include-fixer--sentinel stdin stdout nil
callback))
process))
(defun clang-include-fixer--command (args)
"Return the clang-include-fixer command line.
Returns a list; the first element is the binary to
execute (`clang-include-fixer-executable'), and the remaining
elements are the command line arguments. Adds proper arguments
for `clang-include-fixer-input-format' and
`clang-include-fixer-init-string'. Appends the current buffer's
file name; prepends ARGS directly in front of it."
(cl-check-type args list)
`(,clang-include-fixer-executable
,(format "-db=%s" clang-include-fixer-input-format)
,(format "-input=%s" clang-include-fixer-init-string)
"-stdin"
,@args
,(buffer-file-name)))
(defun clang-include-fixer--sentinel (stdin stdout stderr callback)
"Return a process sentinel for clang-include-fixer processes.
STDIN, STDOUT, and STDERR are buffers for the standard streams;
only STDERR may be nil. CALLBACK is called in the case of
success; it is called with a single argument, STDOUT. On
failure, a buffer containing the error output is displayed."
(cl-check-type stdin buffer-live)
(cl-check-type stdout buffer-live)
(cl-check-type stderr (or null buffer-live))
(cl-check-type callback function)
(lambda (process event)
(cl-check-type process process)
(cl-check-type event string)
(unwind-protect
(if (string-equal event "finished\n")
(progn
(when stderr (kill-buffer stderr))
(with-current-buffer stdin
(funcall callback stdout))
(kill-buffer stdout))
(when stderr (kill-buffer stdout))
(message "clang-include-fixer failed")
(with-current-buffer (or stderr stdout)
(insert "\nProcess " (process-name process)
?\s event))
(display-buffer (or stderr stdout))))
nil))
(defun clang-include-fixer--replace-buffer (stdout)
"Replace current buffer by content of STDOUT."
(cl-check-type stdout buffer-live)
(barf-if-buffer-read-only)
(unless (clang-include-fixer--insert-line stdout (current-buffer))
(erase-buffer)
(insert-buffer-substring stdout))
(message "Fix applied")
nil)
(defun clang-include-fixer--insert-line (from to)
"Insert a single missing line from the buffer FROM into TO.
FROM and TO must be buffers. If the contents of FROM and TO are
equal, do nothing and return non-nil. If FROM contains a single
line missing from TO, insert that line into TO so that the buffer
contents are equal and return non-nil. Otherwise, do nothing and
return nil. Buffer restrictions are ignored."
(cl-check-type from buffer-live)
(cl-check-type to buffer-live)
(with-current-buffer from
(save-excursion
(save-restriction
(widen)
(with-current-buffer to
(save-excursion
(save-restriction
(widen)
;; Search for the first buffer difference.
(let ((chars (abs (compare-buffer-substrings to nil nil from nil nil))))
(if (zerop chars)
;; Buffer contents are equal, nothing to do.
t
(goto-char (point-min))
(forward-char chars)
;; We might have ended up in the middle of a line if the
;; current line partially matches. In this case we would
;; have to insert more than a line. Move to the beginning of
;; the line to avoid this situation.
(beginning-of-line)
(with-current-buffer from
(goto-char (point-min))
(forward-char chars)
(beginning-of-line)
(let ((from-begin (point))
(from-end (progn (forward-line) (point)))
(to-point (with-current-buffer to (point))))
;; Search for another buffer difference after the line in
;; question. If there is none, we can proceed.
(when (zerop (compare-buffer-substrings from from-end nil
to to-point nil))
(with-current-buffer to
(insert-buffer-substring from from-begin from-end))
t))))))))))))
(defun clang-include-fixer--add-header (stdout)
"Analyse the result of include-fixer stored in STDOUT.
Add a missing header if there is any. If there are multiple
possible headers the user can select one of them to be included.
Temporarily highlight the affected symbols. Asynchronously call
clang-include-fixer to insert the selected header."
(cl-check-type stdout buffer-live)
(let ((context (clang-include-fixer--parse-json stdout)))
(let-alist context
(cond
((null .QuerySymbolInfos)
(message "The file is fine, no need to add a header."))
((null .HeaderInfos)
(message "Couldn't find header for '%s'"
(let-alist (car .QuerySymbolInfos) .RawIdentifier)))
(t
;; Replace the HeaderInfos list by a single header selected by
;; the user.
(clang-include-fixer--select-header context)
;; Call clang-include-fixer again to insert the selected header.
(clang-include-fixer--start
#'clang-include-fixer--replace-buffer
(format "-insert-header=%s"
(clang-include-fixer--encode-json context)))))))
nil)
(defun clang-include-fixer--select-header (context)
"Prompt the user for a header if necessary.
CONTEXT must be a clang-include-fixer context object in
association list format. If it contains more than one HeaderInfo
element, prompt the user to select one of the headers. CONTEXT
is modified to include only the selected element."
(cl-check-type context cons)
(let-alist context
(if (cdr .HeaderInfos)
(clang-include-fixer--prompt-for-header context)
(message "Only one include is missing: %s"
(let-alist (car .HeaderInfos) .Header))))
nil)
(defvar clang-include-fixer--history nil
"History for `clang-include-fixer--prompt-for-header'.")
(defun clang-include-fixer--prompt-for-header (context)
"Prompt the user for a single header.
The choices are taken from the HeaderInfo elements in CONTEXT.
They are replaced by the single element selected by the user."
(let-alist context
(let ((symbol (clang-include-fixer--symbol-name .QuerySymbolInfos))
;; Add temporary highlighting so that the user knows which
;; symbols the current session is about.
(overlays (mapcar #'clang-include-fixer--highlight .QuerySymbolInfos)))
(unwind-protect
(save-excursion
;; While prompting, go to the closest overlay so that the user sees
;; some context.
(goto-char (clang-include-fixer--closest-overlay overlays))
(cl-flet ((header (info) (let-alist info .Header)))
;; The header-infos is already sorted by include-fixer.
(let* ((header (ido-completing-read
(format-message "Select include for '%s': "
symbol)
(mapcar #'header .HeaderInfos)
nil :require-match nil
'clang-include-fixer--history))
(info (cl-find header .HeaderInfos :key #'header)))
(cl-assert info)
(setcar .HeaderInfos info)
(setcdr .HeaderInfos nil))))
(mapc #'delete-overlay overlays)))))
(defun clang-include-fixer--symbol-name (symbol-infos)
"Return the unique symbol name in SYMBOL-INFOS.
Raise a signal if the symbol name is not unique."
(let ((symbols (delete-dups (mapcar (lambda (info)
(let-alist info .RawIdentifier))
symbol-infos))))
(when (cdr symbols)
(error "Multiple symbols %s returned" symbols))
(car symbols)))
(defun clang-include-fixer--highlight (symbol-info)
"Add an overlay to highlight SYMBOL-INFO.
Return the overlay object."
(let ((overlay (let-alist symbol-info
(make-overlay
(clang-include-fixer--filepos-to-bufferpos
.Range.Offset 'approximate)
(clang-include-fixer--filepos-to-bufferpos
(+ .Range.Offset .Range.Length) 'approximate)))))
(overlay-put overlay 'face 'clang-include-fixer-highlight)
overlay))
(defun clang-include-fixer--closest-overlay (overlays)
"Return the start of the overlay in OVERLAYS that is closest to point."
(cl-check-type overlays cons)
(let ((point (point))
acc)
(dolist (overlay overlays acc)
(let ((start (overlay-start overlay)))
(when (or (null acc) (< (abs (- point start)) (abs (- point acc))))
(setq acc start))))))
(defun clang-include-fixer--parse-json (buffer)
"Parse a JSON response from clang-include-fixer in BUFFER.
Return the JSON object as an association list."
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(let ((json-object-type 'alist)
(json-array-type 'list)
(json-key-type 'symbol)
(json-false :json-false)
(json-null nil)
(json-pre-element-read-function nil)
(json-post-element-read-function nil))
(json-read)))))
(defun clang-include-fixer--encode-json (object)
"Return the JSON representation of OBJECT as a string."
(let ((json-encoding-separator ",")
(json-encoding-default-indentation " ")
(json-encoding-pretty-print nil)
(json-encoding-lisp-style-closings nil)
(json-encoding-object-sort-predicate nil))
(json-encode object)))
(defun clang-include-fixer--symbol-at-point ()
"Return the qualified symbol at point.
If there is no symbol at point, return nil."
;; Let bounds-of-thing-at-point to do the hard work and deal with edge
;; cases.
(let ((bounds (bounds-of-thing-at-point 'symbol)))
(when bounds
(let ((beg (car bounds))
(end (cdr bounds)))
(save-excursion
;; Extend the symbol range to the left. Skip over namespace
;; delimiters and parent namespace names.
(goto-char beg)
(while (and (clang-include-fixer--skip-double-colon-backward)
(skip-syntax-backward "w_")))
;; Skip over one more namespace delimiter, for absolute names.
(clang-include-fixer--skip-double-colon-backward)
(setq beg (point))
;; Extend the symbol range to the right. Skip over namespace
;; delimiters and child namespace names.
(goto-char end)
(while (and (clang-include-fixer--skip-double-colon-forward)
(skip-syntax-forward "w_")))
(setq end (point)))
(buffer-substring-no-properties beg end)))))
(defun clang-include-fixer--skip-double-colon-forward ()
"Skip a double colon.
When the next two characters are '::', skip them and return
non-nil. Otherwise return nil."
(let ((end (+ (point) 2)))
(when (and (<= end (point-max))
(string-equal (buffer-substring-no-properties (point) end) "::"))
(goto-char end)
t)))
(defun clang-include-fixer--skip-double-colon-backward ()
"Skip a double colon.
When the previous two characters are '::', skip them and return
non-nil. Otherwise return nil."
(let ((beg (- (point) 2)))
(when (and (>= beg (point-min))
(string-equal (buffer-substring-no-properties beg (point)) "::"))
(goto-char beg)
t)))
;; filepos-to-bufferpos is new in Emacs 25.1. Provide a fallback for older
;; versions.
(defalias 'clang-include-fixer--filepos-to-bufferpos
(if (fboundp 'filepos-to-bufferpos)
'filepos-to-bufferpos
(lambda (byte &optional _quality _coding-system)
(byte-to-position (1+ byte)))))
(provide 'clang-include-fixer)
;;; clang-include-fixer.el ends here