;;; smart-dash.el --- Smart-Dash minor mode

;; Copyright (C) 2008-2010 Dennis Lambe Jr.

;; Author: Dennis Lambe Jr. <malsyned@malsyned.net>
;; Webpage: http://malsyned.net/smart-dash.html

;; Smart-Dash 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 2 of the License, or
;; (at your option) any later version.

;; Smart-Dash 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 Smart-Dash.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; Smart-Dash mode is a minor mode which redefines the dash key to
;; insert an underscore within C-style identifiers and a dash
;; otherwise.

;;; Code:

(defgroup smart-dash nil
  "Intelligently insert either a dash or an underscore depending
on context."
  :group 'convenience
  :prefix "smart-dash")

(defcustom smart-dash-c-modes '(c-mode c++-mode objc-mode)
  ; Remove definition with (makunbound 'smart-dash-c-modes)
  "Major modes in which _> should be replaced by the -> struct
pointer member access operator and __ should be replaced by the
-- post-decrement operator."
  :group 'smart-dash
  :type '(repeat symbol))

;;; Smart-Dash mode Keymaps and Mode Functions ;;;

;; I tried also mapping "_" to the inverse operation, but it made it
;; much more awkward to type double-underscored identifiers like
;; __attribute__.  The default Emacs C-q escape works almost as well
;; and doesn't have nasty interactions with common cases.
;; <kp-subtract> on the numeric keypad can also be used to always
;; insert a dash.
(easy-mmode-defmap smart-dash-mode-keymap
   ; Remove definition with (makunbound 'smart-dash-mode-keymap)
   `(("-" . smart-dash-insert)
     (,(kbd "<kp-subtract>") . smart-dash-insert-dash))
   "Key map for `smart-dash-mode'.")

;;;###autoload
(define-minor-mode smart-dash-mode
  "Redefine the dash key to insert an underscore within C-style
identifiers and a dash otherwise.  This allows you to type
all_lowercase_c_identifiers as comfortably as you would
lisp-style-identifiers.

While Smart-Dash mode is active, you can type \\[quoted-insert] -
or use the minus key on the numeric keypad to override it and
insert a dash after a C-style identifier character.  You might
need to do this if you want to type a cramped-looking expression
like x-5.

If Smart-Dash mode is activated while in a C-like mode (c-mode,
c++-mode, and objc-mode by default, customizable with
`smart-dash-c-modes') it will also activate Smart-Dash-C mode,
which translates \"_>\" into \"->\" and \"__\" into \"--\"
automatically so that struct pointer member access and
postfix-decrement aren't made more difficult by Smart-Dash mode's
tendency to insert underscores at the tail ends of identifiers
whether you want it to or not.  Note that this will necessitate
that you type literal underscores if you want more than one
underscore in a row."
  nil "" smart-dash-mode-keymap
  (if smart-dash-mode
      (progn
        (and (memq major-mode smart-dash-c-modes)
             (smart-dash-c-mode 1))
        (smart-dash-isearch-install))
    (smart-dash-isearch-uninstall)
    (smart-dash-c-mode 0)))

(easy-mmode-defmap smart-dash-c-mode-keymap
   ; Remove definition with (makunbound 'smart-dash-c-mode-keymap)
   '((">" . smart-dash-insert-gt))
   "Key map supplement for `smart-dash-mode' when in a C-like
major mode.  See `smart-dash-c-modes'")

(define-minor-mode smart-dash-c-mode
  "Set the > key to call `smart-dash-insert-gt'.  Also modifies
the behavior of the dash key so that the postfix-- operator can
be typed normally (but shift will be needed for typing more than
one underscore in a row).

DO NOT ACTIVATE THIS MINOR MODE DIRECTLY.  Smart-Dash mode will
activate it if the current major mode is listed in
`smart-dash-c-modes'."
  nil "" smart-dash-c-mode-keymap
  (if smart-dash-c-mode
      (smart-dash-c-isearch-install)
    (smart-dash-c-isearch-uninstall)))

;; in-include code thanks to Josh Huber
(defun smart-dash-in-regular-code-p ()
  (let* ((syntax-ppss (syntax-ppss))
         (in-string (nth 3 syntax-ppss))
         (in-comment (nth 4 syntax-ppss))
         (in-include (and smart-dash-c-mode
                          (looking-back " *# *include +[<\"].*"
                                        (save-excursion (beginning-of-line)
                                                        (point))))))
    (and (not in-string)
         (not in-comment)
         (not in-include))))

(defun smart-dash-char-before-point (&optional n)
  (when (not n)
    (setq n 0))
  (char-before (+ (point) n)))

(defun smart-dash-insert-or-overwrite (text)
  "Insert or overwrite TEXT depending of overwrite-mode status.
TEST can be a single character or a string.
Multi-byte characters are handled properly."
  (when overwrite-mode
    (if (stringp text)
        (delete-char (length text))
      (delete-char 1)))
  (insert text))

;; This function makes all of the decisions about whether to insert a
;; dash or an underscore, and how to change the preceeding text if
;; necessary.  It takes all buffer-inspection and buffer-modification
;; functions as arguments so that it can be used both to edit the
;; actual buffer and to edit the isearch-string.
(defun smart-dash-do-insert (insertf deletef bobpf char-before-f regcodepf)
  (let ((ident-re "[A-Za-z0-9_]")
        (dash-or-underscore "-"))
    (when (and (funcall regcodepf)
               (not (funcall bobpf)))
      (do-c-fixup insertf deletef char-before-f)
      (when (string-match ident-re (string (funcall char-before-f)))
        (setf dash-or-underscore "_")))
    (funcall insertf dash-or-underscore)))

(defun do-c-fixup (insertf deletef char-before-f)
  (when smart-dash-c-mode
    (cond ((eql ?_ (funcall char-before-f))
           (funcall deletef 1)
           (funcall insertf "-"))
          ((and (eql ?- (funcall char-before-f))
                (eql ?- (funcall char-before-f -1)))
           (funcall deletef 2)
           (funcall insertf "_-")))))

(defun smart-dash-insert ()
  "Insert an underscore following [A-Za-z0-9_], a dash otherwise.

If `smart-dash-c-mode' is activated, also replace __ with --."
  (interactive)
  (smart-dash-do-insert 'smart-dash-insert-or-overwrite
                        'delete-backward-char
                        'bobp
                        'smart-dash-char-before-point
                        'smart-dash-in-regular-code-p))

(defun smart-dash-insert-dash ()
  "Insert a dash regardless of the preceeding character."
  (interactive)
  (smart-dash-insert-or-overwrite ?-))

(defun smart-dash-do-insert-gt (insertf deletef bobpf char-before-f codepf)
  (when (and (funcall codepf)
             (not (funcall bobpf)))
    (do-c-fixup insertf deletef char-before-f))
  (funcall insertf ">"))

(defun smart-dash-insert-gt ()
  "Insert a greater-than symbol.  If the preceeding character is
an underscore, replace it with a dash.

This behavior is desirable in order to make struct pointer member
access comfortable."
  (interactive)
  (smart-dash-do-insert-gt 'smart-dash-insert-or-overwrite
                           'delete-backward-char
                           'bobp
                           'smart-dash-char-before-point
                           'smart-dash-in-regular-code-p))

;;; Smart-Dash iSearch Support ;;;

(defun smart-dash-isearch-in-regular-code-p ()
  ;; Punt
  t)

(defun smart-dash-isearch-string (s)
  (when (not (stringp s))
    (setq s (string s)))
  (mapc (lambda (c)
          (let ((last-command-event c))
            (isearch-printing-char)))
        s))

(defun smart-dash-isearch-del-char (&optional n)
  (if (not n)
      (setq n 1))
  (while (> n 0)
    (isearch-del-char)
    (setq n (1- n))))

(defun smart-dash-isearch-char-before (&optional n)
  (when (not n)
    (setq n 0))
  (condition-case nil
      (elt (substring isearch-string
                      (- n 1)
                      (and (/= n 0) n))
           0)
    (args-out-of-range nil)))

(defun smart-dash-isearch-bobp ()
  (= 0 (length isearch-string)))

(defun smart-dash-isearch-insert ()
  "Isearch for an underscore following [A-Za-z0-9_], a dash otherwise.

If `smart-dash-c-mode' is activated, also replace __ with -- in
isearches."
  (interactive)
  (smart-dash-do-insert 'smart-dash-isearch-string
                        'smart-dash-isearch-del-char
                        'smart-dash-isearch-bobp
                        'smart-dash-isearch-char-before
                        'smart-dash-isearch-in-regular-code-p))

(defun smart-dash-isearch-insert-dash ()
  "Isearch for a dash regardless of the preceeding character."
  (interactive)
  (smart-dash-isearch-string ?-))

(defun smart-dash-isearch-install ()
  (let ((map isearch-mode-map))
    (make-local-variable 'isearch-mode-map)
    (setq isearch-mode-map (copy-keymap map)))
  (define-key isearch-mode-map "-" 'smart-dash-isearch-insert)
  (define-key
    isearch-mode-map
    (kbd "<kp-subtract>")
    'smart-dash-isearch-insert-dash))

;; The surest, cleanest way to undo the work of
;; `smart-dash-isearch-install' would be to delete the buffer-local
;; binding for isearch-mode-map, but if other modes have also
;; overridden some isearch-mode-map bindings then that would clobber
;; them as well.  So we leave the local binding alone and just set our
;; bindings back to their default values
(defun smart-dash-isearch-uninstall ()
  (when (local-variable-p 'isearch-mode-map)
    (define-key isearch-mode-map "-" 'isearch-printing-char)
    (define-key isearch-mode-map
      (kbd "<kp-subtract>") 'isearch-printing-char)))

(defun smart-dash-isearch-insert-gt ()
  "Isearch for a greater-than symbol.  If the preceeding
character is an underscore, replace it with a dash.

This behavior is desirable in order to make searching for struct
pointer member access comfortable."
  (interactive)
  (smart-dash-do-insert-gt 'smart-dash-isearch-string
                           'smart-dash-isearch-del-char
                           'smart-dash-isearch-bobp
                           'smart-dash-isearch-char-before
                           'smart-dash-isearch-in-regular-code-p))

(defun smart-dash-c-isearch-install ()
  (let ((map isearch-mode-map))
    (make-local-variable 'isearch-mode-map)
    (setq isearch-mode-map (copy-keymap map)))
  (define-key isearch-mode-map ">" 'smart-dash-isearch-insert-gt))

;; See comments on smart-dash-isearch-uninstall
(defun smart-dash-c-isearch-uninstall ()
  (when (local-variable-p 'isearch-mode-map)
    (define-key isearch-mode-map ">" 'isearch-printing-char)))

;;; Smart-Dash MiniBuffer Support ;;;

;; Turning on Smart-Dash mode in all minibuffers started from a
;; particular buffer seems like a good idea until you actually try
;; it. At that point you realize that it's a nuisance as often as a
;; help - for example, in execute-extended-command and eval-expression
;; minibuffers where you're typing lisp identifiers and expecting
;; dashes to stay dashes.
;;
;; The only way I've been able to come up with for getting around this
;; difficulty is with Allow/Deny lists for commands.  By default,
;; Smart-Dash mode will only come on in minibuffers started by
;; commands in smart-dash-minibuffer-allow-commands.  I encourage my
;; users to send me examples of commands that they add to this list.
;;
;; If you'd like to have Smart-Dash mode on in all minibuffers started
;; from a Smart-Dash buffer, set smart-dash-minibuffer-by-default to t
;; and tweak smart-dash-minibuffer-deny-commands to add exceptions.
;;
;; If you hate this feature altogether, set
;; smart-dash-minibuffer-enabled to nil.

(defgroup smart-dash-minibuffer nil
  "Activate Smart-Dash mode in the minibuffer
for commands that are executed in Smart-Dash mode buffers."
  :prefix "smart-dash-minibuffer"
  :group 'smart-dash)

(defcustom smart-dash-minibuffer-enabled t
  ; Remove definition with (makunbound 'smart-dash-minibuffer-enabled)
  "If non-nil, activate Smart-Dash mode in the minibuffer for
some commands.

The decision of which commands will have Smart-Dash mode
activated in is controlled by `smart-dash-minibuffer-by-default',
`smart-dash-minibuffer-allow-commands' and
`smart-dash-minibuffer-deny-commands'."
  :group 'smart-dash-minibuffer
  :type 'boolean)

(defcustom smart-dash-minibuffer-by-default nil
  ; Remove definition with (makunbound 'smart-dash-minibuffer-by-default)
  "If non-nil, activate Smart-Dash mode in the minibuffer for all
commands except those listed in `smart-dash-minibuffer-deny-commands'.

If nil (the default), activate Smart-Dash mode in the minibuffer
only for commands listed in `smart-dash-minibuffer-allow-commands'."
  :group 'smart-dash-minibuffer
  :type 'boolean)

(defcustom smart-dash-minibuffer-allow-commands
  '(query-replace
    query-replace-regexp
    replace-string
    replace-regexp
    search-forward
    search-forward-regexp
    isearch-edit-string
    string-rectangle
    find-tag
    grep)
  ; Remove definition with
  ; (makunbound 'smart-dash-minibuffer-allow-commands)
  "Commands whose minibuffer inputs should have Smart-Dash mode
activated when `smart-dash-minibuffer-by-default' is nil."
  :group 'smart-dash-minibuffer
  :type '(repeat function))

(defcustom smart-dash-minibuffer-deny-commands
  '(execute-extended-command
    eval-expression
    find-file)
  ; Remove definition with
  ; (makunbound 'smart-dash-minibuffer-deny-commands)
  "Commands whose minibuffer inputs should NOT have Smart-Dash
mode activated when `smart-dash-minibuffer-by-default' is
non-nil."
  :group 'smart-dash-minibuffer
  :type '(repeat function))

;; When a command uses the minibuffer more than once, all but the
;; first will have `this-command' set to the command which exited the
;; previous minibuffer.  Usually this is `exit-minibuffer' or
;; `self-insert-and-exit', but there's no way to know for certain what
;; commands may cause the exit.  When this occurs it prevents
;; `this-command' from matching the Allow and Deny command lists.
;;
;; The solution used by Smart-Dash minibuffer support is to record the
;; command that caused the previous minibuffer exit and, if that's the
;; command that invoked the current minibuffer, to use the previous
;; value of `this-command' to determine whether Smart-Dash minibuffer
;; support should be enabled.

;; The possibility of recursive minibuffers necessitate using stacks
;; rather than simple variables to deduce the current minibffer-using
;; command.
(defvar smart-dash-minibuffer-last-exit-command-stack (list nil))
(defvar smart-dash-minibuffer-this-command-stack (list nil))

(defun smart-dash-minibuffer-exit ()
  (pop smart-dash-minibuffer-last-exit-command-stack)
  (pop smart-dash-minibuffer-this-command-stack)
  (rplaca smart-dash-minibuffer-last-exit-command-stack this-command))

(add-hook 'minibuffer-exit-hook 'smart-dash-minibuffer-exit)

(defun smart-dash-minibuffer-install ()
  ;; Initialize stacks if they are nil
  (unless smart-dash-minibuffer-this-command-stack
    (setq smart-dash-minibuffer-this-command-stack (list this-command)))
  (unless smart-dash-minibuffer-last-exit-command-stack
    (setq smart-dash-minibuffer-last-exit-command-stack (list this-command)))

  (unless (eq this-command
              (car smart-dash-minibuffer-last-exit-command-stack))
    (rplaca smart-dash-minibuffer-this-command-stack this-command))
  (let* ((selected-buffer (window-buffer (minibuffer-selected-window)))
         (sd-active
          (with-current-buffer selected-buffer smart-dash-mode))
         (sd-c-active
          (with-current-buffer selected-buffer smart-dash-c-mode))
         (local-this-command
          (car smart-dash-minibuffer-this-command-stack))
         (allow (and smart-dash-minibuffer-enabled
                     (if smart-dash-minibuffer-by-default
                         (not (memq local-this-command
                                    smart-dash-minibuffer-deny-commands))
                       (memq local-this-command
                             smart-dash-minibuffer-allow-commands)))))
    (when allow
      (if sd-active
          (smart-dash-mode))
      (if sd-c-active
          (smart-dash-c-mode))))
  (push nil smart-dash-minibuffer-last-exit-command-stack)
  (push nil smart-dash-minibuffer-this-command-stack))

(add-hook 'minibuffer-setup-hook 'smart-dash-minibuffer-install)

(provide 'smart-dash)