;;; macrostep-c.el --- macrostep interface to C preprocessor

;; Copyright (C) 2015 Jon Oddie <j.j.oddie@gmail.com>

;; Author:     Jon Oddie <j.j.oddie@gmail.com>
;; Maintainer: Jon Oddie <j.j.oddie@gmail.com>
;; Created:    27 November 2015
;; Updated:    27 November 2015
;; Version:    0.9
;; Keywords:   c, languages, macro, debugging
;; Url:        https://github.com/joddie/macrostep

;; This file is NOT part of GNU Emacs.

;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see `http://www.gnu.org/licenses/'.

;;; Commentary:

;; A thin wrapper around Emacs's built-in `cmacexp' library to provide
;; basic support for expanding C macros using the `macrostep' user
;; interface.  To use, position point on a macro use in a C buffer and
;; type `M-x macrostep-expand'.  The variables `c-macro-preprocessor'
;; and especially `c-macro-cppflags' may need to be set correctly for
;; accurate expansion.

;; This is fairly basic compared to the Emacs Lisp `macrostep'.  In
;; particular, there is no step-by-step expansion, since C macros are
;; expanded in a single "cpp" pass, and no pretty-printing.

;; To hide the buffer containing "cpp" warnings (not recommended), you
;; could do something like:
;;
;; (push `(,(regexp-quote macrostep-c-warning-buffer)
;;          (display-buffer-no-window))
;;       display-buffer-alist)

;;; Code:

(require 'macrostep)
(require 'cmacexp)
(require 'cl-lib)

(eval-and-compile
  (if (require 'subr-x nil t)
      (defalias 'macrostep-c-string-trim 'string-trim)
    (defun macrostep-c-string-trim (string)
      (when (string-match "\\`[ \t\n\r]+" string)
	(setq string (replace-match "" t t string)))
      (when (string-match "[ \t\n\r]+\\'" string)
	(setq string (replace-match "" t t string)))
	string)))

(put 'macrostep-c-non-macro 'error-conditions
     '(macrostep-c-non-macro error))
(put 'macrostep-c-non-macro 'error-message
     "Text around point is not a macro call.")

(put 'macrostep-c-expansion-failed 'error-conditions
     '(macrostep-c-expansion-failed error))
(put 'macrostep-c-expansion-failed 'error-message
     "Macro-expansion failed.")

(defvar macrostep-c-warning-buffer "*Macroexpansion Warnings*")

;;;###autoload
(defun macrostep-c-mode-hook ()
  (setq macrostep-sexp-bounds-function
        #'macrostep-c-sexp-bounds)
  (setq macrostep-sexp-at-point-function
        #'macrostep-c-sexp-at-point)
  (setq macrostep-environment-at-point-function
        #'ignore)
  (setq macrostep-expand-1-function
        #'macrostep-c-expand-1)
  (setq macrostep-print-function
        #'macrostep-c-print-function)
  (add-hook 'macrostep-mode-off-hook
            #'macrostep-c-mode-off nil t))

(defun macrostep-c-mode-off (&rest ignore)
  (when (derived-mode-p 'c-mode)
    (let ((warning-window
           (get-buffer-window macrostep-c-warning-buffer)))
      (when warning-window
        (quit-window nil warning-window)))))

;;;###autoload
(add-hook 'c-mode-hook #'macrostep-c-mode-hook)

(defun macrostep-c-sexp-bounds ()
  (save-excursion
    (cl-loop
     (let ((region (macrostep-c-sexp-bounds-1)))
       (cond
         ((null region)
          (signal 'macrostep-c-non-macro nil))
         ((macrostep-c-expandable-p region)
          (cl-return region))
         (t
          (condition-case nil
              (progn
                (backward-up-list)
                (skip-syntax-backward "-"))
            (scan-error
             (signal 'macrostep-c-non-macro nil)))))))))

(defun macrostep-c-sexp-bounds-1 ()
  (let ((region (bounds-of-thing-at-point 'symbol)))
    (when region
      (cl-destructuring-bind (symbol-start . symbol-end) region
        (save-excursion
          (goto-char symbol-end)
          (if (looking-at "[[:space:]]*(")
              (cons symbol-start (scan-sexps symbol-end 1))
              region))))))

(defun macrostep-c-expandable-p (region)
  (cl-destructuring-bind (start . end) region
    (condition-case nil
        (cl-destructuring-bind (expansion warnings)
            (macrostep-c-expand-region start end)
          (declare (ignore warnings))
          (and (cl-plusp (length expansion))
               (not (string= expansion (buffer-substring start end)))))
      (macrostep-c-expansion-failed nil))))

(defun macrostep-c-sexp-at-point (start end)
  (cons start end))

(defun macrostep-c-expand-1 (region _ignore)
  (cl-destructuring-bind (start . end) region
    (cl-destructuring-bind (expansion warnings)
        (macrostep-c-expand-region start end)
      (when (cl-plusp (length warnings))
        (with-current-buffer
            (get-buffer-create macrostep-c-warning-buffer)
          (let ((inhibit-read-only t))
            (erase-buffer)
            (insert warnings)
            (goto-char (point-min)))
          (special-mode)
          (display-buffer (current-buffer)
                          '(display-buffer-pop-up-window
                            (inhibit-same-window . t)
                            (allow-no-window . t)))))
      expansion)))

(defun macrostep-c-expand-region (start end)
  (let ((expansion
         (condition-case nil
             (c-macro-expansion start end
                                (concat c-macro-preprocessor " "
                                        c-macro-cppflags))
           (search-failed
            (signal 'macrostep-c-expansion-failed nil)))))
    (with-temp-buffer
      (save-excursion
        (insert expansion))
      (when (looking-at (regexp-quote "/*"))
        (search-forward "*/"))
      (let ((warnings (buffer-substring (point-min) (point)))
            (expansion (buffer-substring (point) (point-max))))
        (mapcar #'macrostep-c-string-trim (list expansion warnings))))))

(defun macrostep-c-print-function (expansion &rest _ignore)
  (with-temp-buffer
    (insert expansion)
    (let ((exit-code
           (shell-command-on-region (point-min) (point-max) "indent" nil t)))
      (when (zerop exit-code)
        (setq expansion (macrostep-c-string-trim (buffer-string))))))
  (insert expansion))

(provide 'macrostep-c)

;;; macrostep-c.el ends here
