;;; mouseme.el --- mouse menu with commands that operate on strings  -*- lexical-binding: t; -*-

;; Copyright (C) 1997-2022 by Free Software Foundation, Inc.

;; Author: Howard Melman <howard@silverstream.com>
;; Keywords: mouse, menu

;; This file is part of GNU Emacs.

;;; License:
;;
;; 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:

;; This package provides a command `mouse-me' to be bound to a mouse
;; button.  It pops up a menu of commands that operate on strings or a
;; region.  The string passed to the selected command is the word or
;; symbol clicked on (with surrounding quotes or other punctuation
;; removed), or the region (if either it was just selected with the
;; mouse or if it was active with `transient-mark-mode' on).  If the
;; command accepts a region, the selected region (or the region of the
;; word or symbol clicked on) will be passed to the command.

;; The idea is that for any given string in a buffer you may want to
;; do different things regardless of the mode of the buffer.  URLs
;; now appear in email, news articles, comments in code, and in plain
;; text.  You may want to visit that URL in a browser or you may just
;; want to copy it to the kill-ring.  For an email address you might
;; want to compose mail to it, finger it, look it up in bbdb, copy it to
;; the kill ring.  For a word you may want to spell check it, copy it,
;; change its case, grep for it, etc.  Mouse-me provides a menu to
;; make this easy.

;; The menu popped up is generated by calling the function in the
;; variable `mouse-me-build-menu-function' which defaults to calling
;; `mouse-me-build-menu' which builds the menu from the variable
;; `mouse-me-menu-commands'.  See the documentation for these
;; functions and variables for details.

;; To install, add something like the following to your ~/.emacs:
;;   (require 'mouseme)
;;   (global-set-key [S-mouse-2] 'mouse-me)

;;; Code:

(require 'browse-url)
(require 'thingatpt)

(eval-when-compile (require 'compile))

;;;; Variables

(defgroup mouseme nil
  "Popup menu of commands that work on strings."
  :prefix "mouse-me-"
  :group 'hypermedia)

(defcustom mouse-me-get-string-function 'mouse-me-get-string
  "Function used by `mouse-me' to get string when no region selected.
The default is `mouse-me-get-string' but this variable may commonly
be made buffer local and set to something more appropriate for
a specific mode (e.g., `word-at-point').  The function will be called
with no arguments and with point at where the mouse was clicked.
It can return either the string or to be most efficient, a list of
three elements: the string and the beginning and ending points of the
string in the buffer."
  :type 'function
  :options '(mouse-me-get-string))

(defcustom mouse-me-build-menu-function 'mouse-me-build-menu
  "Function used by `mouse-me' to build the popup menu.
The default is `mouse-me-build-menu' but this variable may commonly
be made buffer local and set to something more appropriate for
a specific mode.  The function will be called with one argument,
the string selected, as returned by `mouse-me-get-string-function'."
  :type 'function
  :options '(mouse-me-build-menu))

(defvar mouse-me-grep-use-extension 't
  "If non-nil `mouse-me-grep' grep's in files with current file's extension.")

(defcustom mouse-me-menu-commands
  '(("Copy" . kill-new)
    ("Kill" . kill-region)
    ("Capitalize" . capitalize-region)
    ("Lowercase" . downcase-region)
    ("Uppercase" . upcase-region)
    ("ISpell" . ispell-region)
    "----"
    ("Browse URL" . browse-url)
    ("Dired" . dired)
    ("Execute File" . mouse-me-execute)
    ("Mail to" . compose-mail)
    ("Finger" . mouse-me-finger)
    ("BBDB Lookup" . mouse-me-bbdb)
    "----"
    ("Imenu" . imenu)
    ("Find Tag" . find-tag)
    ("Grep" . mouse-me-grep)
    ("Find-Grep" . mouse-me-find-grep)
    "----"
    ("Apropos" . apropos)
    ("Describe Function" . mouse-me-describe-function)
    ("Describe Variable" . mouse-me-describe-variable)
    ("Command Info" . mouse-me-emacs-command-info)
    ("Man Page" . (if (fboundp 'woman) 'woman 'man))
    ("Profile Function" . mouse-me-elp-instrument-function))
  "Command menu used by `mouse-me-build-menu'.
A list of elements where each element is either a cons cell or a string.
If a cons cell the car is a string to be displayed in the menu and the
cdr is either a function to call passing a string to, or a list which evals
to a function to call passing a string to.  If the element is a string
it makes a non-selectable element in the menu.  To make a separator line
use a string consisting solely of hyphens.

The function returned from this menu will be called with one string
argument.  Or if the function has the symbol property `mouse-me-type'
and if its value is the symbol `region' it will be called with the
beginning and ending points of the selected string.  If the value is
the symbol `string' it will be called with one string argument."
  :type '(repeat sexp))

(put 'kill-region 'mouse-me-type 'region)
(put 'ispell-region 'mouse-me-type 'region)
(put 'capitalize-region 'mouse-me-type 'region)
(put 'downcase-region 'mouse-me-type 'region)
(put 'upcase-region 'mouse-me-type 'region)

;;;; Commands

;;;###autoload
(defun mouse-me (event)
  "Popup a menu of functions to run on selected string or region."
  (interactive "e")
  (mouse-me-helper event (lambda (name)
                           (or (x-popup-menu event (funcall mouse-me-build-menu-function name))
                               (error "No command to run")))))

;;;; Exposed Functions

;; Some tests:
;; <URL:http://foo.bar.com/sss/ss.html>
;; <http://foo.bar.com/sss/ss.html>
;; http://foo.bar.com/sss/ss.html
;; http://www.ditherdog.com/howard/
;; mailto:howard@silverstream.com
;; howard@silverstream.com
;; <howard@silverstream.com>
;; import com.sssw.srv.agents.AgentsRsrc;
;; public AgoHttpRequestEvent(Object o, String db, Request r)
;; <DIV><A href=3D"http://www.amazon.com/exec/obidos/ASIN/156592391X"><IMG =
;; <A HREF="http://www.suntimes.com/ebert/ebert.html">
;; d:\howard\elisp\spoon
;; \howard\elisp\spoon
;; \\absolut\howard\elisp\spoon
;; //absolut/d/Howard/Specs/servlet-2.1.pdf
;; \\absolut\d\Howard\Specs\servlet-2.1.pdf
;; gnuserv-frame.

(defun mouse-me-get-string ()
  "Return a string from the buffer of text surrounding point.
Returns a list of three elements, the string and the beginning and
ending positions of the string in the buffer in that order."
  (save-match-data
    (save-excursion
      (let ((start (point)) beg end str)
        (skip-syntax-forward "^ >()\"")
        (setq end (point))
        (goto-char start)
        (skip-syntax-backward "^ >()\"")
        (setq beg (point))
        (setq str (buffer-substring-no-properties beg end))
        ;; remove junk from the beginning
        (if (string-match "^\\([][\"'`.,?:;!@#$%^&*()_+={}|<>-]+\\)" str)
            (setq str (substring str (match-end 1))
                  beg (+ beg (match-end 1))))
        ;; remove URL: from the front, it's common in email
        (if (string-match "^\\(URL:\\)" str)
            (setq str (substring str (match-end 1))
                  beg (+ beg (match-end 1))))
        ;; remove junk from the end
        (if (string-match "\\([][\"'.,?:;!@#$%^&*()_+={}|<>-]+\\)$" str)
            (setq end (- end (length (match-string 1 str))) ; must set end first
                  str (substring str 0 (match-beginning 1))))
        (list str beg end)))))

(defun mouse-me-build-menu (name)
  "Return a menu tailored for NAME for `mouse-me' from `mouse-me-menu-commands'."
  (list "Mouse Me" (cons "Mouse Me"
                         (append (list (cons
                                        (if (< (length name) 65)
                                            name
                                          "...Long String...")
                                        'kill-new)
                                       "---")
                                 mouse-me-menu-commands))))

;;;; Commands for the menu

(defun mouse-me-emacs-command-info (string)
  "Look in Emacs info for command named STRING."
  (interactive "sCommand: ")
  (let ((s (intern-soft string)))
    (if (and s (commandp s))
        (Info-goto-emacs-command-node s)
      (error "No command named `%s'" string))))

(defun mouse-me-describe-function (string)
  "Describe function named STRING."
  (interactive "sFunction: ")
  (let ((s (intern-soft string)))
    (if (and s (fboundp s))
        (describe-function s)
      (error "No function named `%s'" string))))

(defun mouse-me-describe-variable (string)
  "Desribe variable named STRING."
  (interactive "sVariable: ")
  (let ((s (intern-soft string)))
    (if (and s (boundp s))
        (describe-variable s)
      (error "No variable named `%s'" string))))

(defun mouse-me-elp-instrument-function (string)
  "Instrument Lisp function named STRING."
  (interactive "sFunction: ")
  (let ((s (intern-soft string)))
    (if (and s (fboundp s))
        (elp-instrument-function s)
      (error "Must be the name of an existing Lisp function"))))

(defun mouse-me-execute (string)
  "Execute STRING as a filename."
  (interactive "sFile: ")
  (if (fboundp 'w32-shell-execute)
      (w32-shell-execute "open" (convert-standard-filename string))
    (message "This function currently working only in W32.")))


(defun mouse-me-bbdb (string)
  "Lookup STRING in bbdb."
  (interactive "sBBDB Lookup: ")
  (if (fboundp 'bbdb)
      (bbdb string nil)
    (error "BBDB not loaded")))

(defun mouse-me-finger (string)
  "Finger a STRING mail address."
  (interactive "sFinger: ")
  (save-match-data
    (if (string-match "\\(.*\\)@\\([-.a-zA-Z0-9]+\\)$" string)
        (finger (match-string 1 string) (match-string 2 string))
      (error "Not in user@host form: %s" string))))

(defun mouse-me-grep (string)
  "Grep for a STRING."
  (interactive "sGrep: ")
  (require 'compile)
  (grep-compute-defaults)
  (let ((ext (mouse-me-buffer-file-extension)))
    (grep (concat grep-command string
                  (if mouse-me-grep-use-extension
                      (if ext
                          (concat " *" ext)
                        " *"))))))

(defun mouse-me-find-grep (string)
  "Grep for a STRING."
  (interactive "sGrep: ")
  (grep-compute-defaults)
  (let ((reg grep-find-command)
        (ext (mouse-me-buffer-file-extension)))
    (if (string-match "\\(^.+-type f \\)\\(.+$\\)" reg)
        (setq reg (concat (match-string 1 reg)
                          (if mouse-me-grep-use-extension
                              (concat "-name \"*" ext "\" "))
                          (match-string 2 reg))))
    (grep-find (concat reg string))))

;;;; Internal Functions

(defun mouse-me-buffer-file-extension ()
  "Return the extension of the current buffer's filename or nil.
Returned extension is a string beginning with a period."
  (let* ((bfn (buffer-file-name))
         (filename (and bfn (file-name-sans-versions bfn)))
         (index (and filename (string-match "\\.[^.]*$" filename))))
    (if index
        (substring filename index)
      "")))

(defun mouse-me-helper (event func)
  "Determine the string to use to process EVENT and call FUNC to get cmd."
  (let (name sp sm mouse beg end cmd mmtype)
    ;; temporarily goto where the event occurred, get the name clicked
    ;; on and enough info to figure out what to do with it
    (save-match-data
      (save-excursion
        (setq sp (point))               ; saved point
        (setq sm (mark t))              ; saved mark
        (set-buffer (window-buffer (posn-window (event-start event))))
        (setq mouse (goto-char (posn-point (event-start event))))
        ;; if there is a region and point is inside it
        ;; check for sm first incase (null (mark t))
        ;; set name to either the thing they clicked on or region
        (if (and sm
                 (or (and transient-mark-mode mark-active)
                     (eq last-command 'mouse-drag-region))
                 (>= mouse (setq beg (min sp sm)))
                 (<= mouse (setq end (max sp sm))))
            (setq name (buffer-substring beg end))
          (setq name (funcall mouse-me-get-string-function))
          (if (listp name)
              (setq beg (nth 1 name)
                    end (nth 2 name)
                    name (car name))
            (goto-char mouse)
            (while (not (looking-at (regexp-quote name)))
              (backward-char 1))
            (setq beg (point))
            (setq end (search-forward name))))))
    ;; check if name is null, meaning they clicked on no word
    (if (or (null name)
            (and (stringp name) (string= name "" )))
        (error "No string to pass to function"))
    ;; popup a menu to get a command to run
    (setq cmd (funcall func))
    ;; run the command, eval'ing if it was a list
    (if (listp cmd)
        (setq cmd (eval cmd t)))
    (setq mmtype (get cmd 'mouse-me-type))
    (cond ((eq mmtype 'region)
           (funcall cmd beg end))
          ((eq mmtype 'string)
           (funcall cmd name))
          (t
           (funcall cmd name)))))

(provide 'mouseme)

;;; mouseme.el ends here
