;;; helm-gtags.el --- GNU GLOBAL helm interface -*- lexical-binding: t; -*-

;; Copyright (C) 2016 by Syohei YOSHIDA

;; Author: Syohei YOSHIDA <syohex@gmail.com>
;; URL: https://github.com/syohex/emacs-helm-gtags
;; Package-Version: 20160417.555
;; Version: 1.5.6
;; Package-Requires: ((emacs "24.3") (helm "1.7.7"))

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

;; `helm-gtags.el' is a `helm' interface of GNU Global.
;; `helm-gtags.el' is not compatible `anything-gtags.el', but `helm-gtags.el'
;; is designed for fast search.

;;
;; To use this package, add these lines to your init.el or .emacs file:
;;
;;     ;; Enable helm-gtags-mode
;;     (add-hook 'c-mode-hook 'helm-gtags-mode)
;;     (add-hook 'c++-mode-hook 'helm-gtags-mode)
;;     (add-hook 'asm-mode-hook 'helm-gtags-mode)
;;
;;     ;; Set key bindings
;;     (eval-after-load "helm-gtags"
;;       '(progn
;;          (define-key helm-gtags-mode-map (kbd "M-t") 'helm-gtags-find-tag)
;;          (define-key helm-gtags-mode-map (kbd "M-r") 'helm-gtags-find-rtag)
;;          (define-key helm-gtags-mode-map (kbd "M-s") 'helm-gtags-find-symbol)
;;          (define-key helm-gtags-mode-map (kbd "M-g M-p") 'helm-gtags-parse-file)
;;          (define-key helm-gtags-mode-map (kbd "C-c <") 'helm-gtags-previous-history)
;;          (define-key helm-gtags-mode-map (kbd "C-c >") 'helm-gtags-next-history)
;;          (define-key helm-gtags-mode-map (kbd "M-,") 'helm-gtags-pop-stack)))
;;

;;; Code:

(require 'cl-lib)
(require 'helm)
(require 'helm-files)
(require 'which-func)
(require 'pulse)

(declare-function helm-comp-read "helm-mode")
(declare-function cygwin-convert-file-name-from-windows "cygw32.c")
(declare-function cygwin-convert-file-name-to-windows "cygw32.c")

(defgroup helm-gtags nil
  "GNU GLOBAL for helm."
  :group 'helm)

(defcustom helm-gtags-path-style 'root
  "Style of file path"
  :type '(choice (const :tag "Root of the current project" root)
                 (const :tag "Relative from the current directory" relative)
                 (const :tag "Absolute Path" absolute)))

(defcustom helm-gtags-ignore-case nil
  "Ignore case in each search."
  :type 'boolean)

(defcustom helm-gtags-read-only nil
  "Gtags read only mode."
  :type 'boolean)

(defcustom helm-gtags-auto-update nil
  "*If non-nil, tag files are updated whenever a file is saved."
  :type 'boolean)

(defcustom helm-gtags-pulse-at-cursor t
  "If non-nil, pulse at point after jumping"
  :type 'boolean)

(defcustom helm-gtags-cache-select-result nil
  "*If non-nil, results of helm-gtags-select and helm-gtags-select-path are cached."
  :type 'boolean)

(defcustom helm-gtags-cache-max-result-size (* 10 1024 1024) ;10M
  "Max size(bytes) to cache for each select result."
  :type 'integer)

(defcustom helm-gtags-update-interval-second 60
  "Tags are updated in `after-save-hook' if this seconds is passed from last update.
Always update if value of this variable is nil."
  :type '(choice (integer :tag "Update interval seconds")
                 (boolean :tag "Update every time" nil)))

(defcustom helm-gtags-highlight-candidate t
  "Highlight candidate or not"
  :type 'boolean)

(defcustom helm-gtags-use-input-at-cursor nil
  "Use input at cursor"
  :type 'boolean)

(defcustom helm-gtags-prefix-key "\C-c"
  "If non-nil, it is used for the prefix key of gtags-xxx command."
  :type 'string)

(defcustom helm-gtags-suggested-key-mapping nil
  "If non-nil, suggested key mapping is enabled."
  :type 'boolean)

(defcustom helm-gtags-preselect nil
  "If non-nil, preselect current file and line."
  :type 'boolean)

(defcustom helm-gtags-display-style nil
  "Style of display result."
  :type '(choice (const :tag "Show in detail" detail)
                 (const :tag "Normal style" nil)))

(defcustom helm-gtags-fuzzy-match nil
  "Enable fuzzy match"
  :type 'boolean)

(defcustom helm-gtags-maximum-candidates (if helm-gtags-fuzzy-match 100 9999)
  "Maximum number of helm candidates"
  :type 'integer)

(defcustom helm-gtags-direct-helm-completing nil
  "Use helm mode directly."
  :type 'boolean)

(defface helm-gtags-file
  '((t :inherit font-lock-keyword-face))
  "Face for line numbers in the error list.")

(defface helm-gtags-lineno
  '((t :inherit font-lock-doc-face))
  "Face for line numbers in the error list.")

(defface helm-gtags-match
  '((t :inherit helm-match))
  "Face for word matched against tagname")

(defvar helm-gtags--tag-location nil)
(defvar helm-gtags--last-update-time 0)
(defvar helm-gtags--completing-history nil)
(defvar helm-gtags--context-stack (make-hash-table :test 'equal))
(defvar helm-gtags--result-cache (make-hash-table :test 'equal))
(defvar helm-gtags--saved-context nil)
(defvar helm-gtags--use-otherwin nil)
(defvar helm-gtags--local-directory nil)
(defvar helm-gtags--parsed-file nil)
(defvar helm-gtags--current-position nil)
(defvar helm-gtags--real-tag-location nil)
(defvar helm-gtags--last-input nil)
(defvar helm-gtags--query nil)
(defvar helm-gtags--last-default-directory nil)

(defconst helm-gtags--buffer "*helm gtags*")

(defconst helm-gtags--include-regexp
  "\\`\\s-*#\\(?:include\\|import\\)\\s-*[\"<]\\(?:[./]*\\)?\\(.*?\\)[\">]")

(defmacro helm-declare-obsolete-variable (old new version)
  `(progn
     (defvaralias ,old ,new)
     (make-obsolete-variable ,old ,new ,version)))

(helm-declare-obsolete-variable
 'helm-c-gtags-path-style 'helm-gtags-path-style "0.8")
(helm-declare-obsolete-variable
 'helm-c-gtags-ignore-case 'helm-gtags-ignore-case  "0.8")
(helm-declare-obsolete-variable
 'helm-c-gtags-read-only 'helm-gtags-read-only "0.8")

;; completsion function for completing-read.
(defun helm-gtags--completing-gtags (string predicate code)
  (helm-gtags--complete 'tag string predicate code))
(defun helm-gtags--completing-pattern (string predicate code)
  (helm-gtags--complete 'pattern string predicate code))
(defun helm-gtags--completing-grtags (string predicate code)
  (helm-gtags--complete 'rtag string predicate code))
(defun helm-gtags--completing-gsyms (string predicate code)
  (helm-gtags--complete 'symbol string predicate code))
(defun helm-gtags--completing-files (string predicate code)
  (helm-gtags--complete 'find-file string predicate code))

(defconst helm-gtags-comp-func-alist
  '((tag       . helm-gtags--completing-gtags)
    (pattern   . helm-gtags--completing-pattern)
    (rtag      . helm-gtags--completing-grtags)
    (symbol    . helm-gtags--completing-gsyms)
    (find-file . helm-gtags--completing-files)))

(defconst helm-gtags--search-option-alist
  '((pattern   . "-g")
    (rtag      . "-r")
    (symbol    . "-s")
    (find-file . "-Poa")))

(defsubst helm-gtags--windows-p ()
  (memq system-type '(windows-nt ms-dos)))

(defun helm-gtags--remove-carrige-returns ()
  (when (helm-gtags--windows-p)
    (save-excursion
      (goto-char (point-min))
      (while (re-search-forward "\xd" nil t)
        (replace-match "")))))

;; Work around for GNU global Windows issue
(defsubst helm-gtags--use-abs-path-p (gtagslibpath)
  (and (helm-gtags--windows-p) gtagslibpath))

(defun helm-gtags--construct-options (type completion)
  (let ((find-file-p (eq type 'find-file))
        (gtagslibpath (getenv "GTAGSLIBPATH"))
        options)
    (unless find-file-p
      (push "--result=grep" options))
    (when completion
      (push "-c" options))
    (helm-aif (assoc-default type helm-gtags--search-option-alist)
        (push it options))
    (when (or (eq helm-gtags-path-style 'absolute)
              (helm-gtags--use-abs-path-p gtagslibpath))
      (push "-a" options))
    (when helm-gtags-ignore-case
      (push "-i" options))
    (when (and current-prefix-arg (not find-file-p))
      (push "-l" options))
    (when gtagslibpath
      (push "-T" options))
    options))

(defun helm-gtags--complete (type string predicate code)
  (let* ((options (helm-gtags--construct-options type t))
         (args (reverse (cons string options)))
         candidates)
    (with-temp-buffer
      (apply #'process-file "global" nil t nil args)
      (goto-char (point-min))
      (while (re-search-forward "^\\(.+\\)$" nil t)
        (push (match-string-no-properties 1) candidates)))
    (if (not code)
        (try-completion string candidates predicate)
      (all-completions string candidates predicate))))

(defun helm-gtags--token-at-point (type)
  (if (not (eq type 'find-file))
      (thing-at-point 'symbol)
    (let ((line (helm-current-line-contents)))
      (when (string-match helm-gtags--include-regexp line)
        (match-string-no-properties 1 line)))))

(defconst helm-gtags--prompt-alist
  '((tag       . "Find Definition: ")
    (pattern   . "Find Pattern: ")
    (rtag      . "Find Reference: ")
    (symbol    . "Find Symbol: ")
    (find-file . "Find File: ")))

(defun helm-gtags--read-tagname (type &optional default-tagname)
  (let ((tagname (helm-gtags--token-at-point type))
        (prompt (assoc-default type helm-gtags--prompt-alist))
        (comp-func (assoc-default type helm-gtags-comp-func-alist)))
    (if (and tagname helm-gtags-use-input-at-cursor)
        tagname
      (when (and (not tagname) default-tagname)
        (setq tagname default-tagname))
      (when tagname
        (setq prompt (format "%s(default \"%s\") " prompt tagname)))
      (let ((completion-ignore-case helm-gtags-ignore-case)
            (completing-read-function 'completing-read-default))
        (if (and helm-gtags-direct-helm-completing (memq type '(tag rtag symbol find-file)))
            (helm-comp-read prompt comp-func
                            :history 'helm-gtags--completing-history
                            :exec-when-only-one t
                            :default tagname)
          (completing-read prompt comp-func nil nil nil
                           'helm-gtags--completing-history tagname))))))

(defun helm-gtags--path-libpath-p (tagroot)
  (helm-aif (getenv "GTAGSLIBPATH")
      (cl-loop for path in (parse-colon-path it)
               for libpath = (file-name-as-directory (expand-file-name path))
               thereis (string= tagroot libpath))))

(defun helm-gtags--tag-directory ()
  (with-temp-buffer
    (helm-aif (getenv "GTAGSROOT")
        it
      (unless (zerop (process-file "global" nil t nil "-p"))
        (error "GTAGS not found"))
      (goto-char (point-min))
      (when (looking-at "^\\([^\r\n]+\\)")
        (let ((tag-path (match-string-no-properties 1)))
          (file-name-as-directory
           (if (eq system-type 'cygwin)
               (cygwin-convert-file-name-from-windows tag-path)
             tag-path)))))))

(defun helm-gtags--find-tag-directory ()
  (setq helm-gtags--real-tag-location nil)
  (let ((tagroot (helm-gtags--tag-directory)))
    (if (and (helm-gtags--path-libpath-p tagroot) helm-gtags--tag-location)
        (progn
          (setq helm-gtags--real-tag-location tagroot)
          helm-gtags--tag-location)
      (setq helm-gtags--tag-location tagroot))))

(defun helm-gtags--base-directory ()
  (let ((dir (or helm-gtags--last-default-directory
                 helm-gtags--local-directory
                 (cl-case helm-gtags-path-style
                   (root (or helm-gtags--real-tag-location
                             helm-gtags--tag-location))
                   (otherwise default-directory))))
        (remote (file-remote-p default-directory)))
    (if (and remote (not (file-remote-p dir)))
        (concat remote dir)
      dir)))

(defsubst helm-gtags--new-context-info (index stack)
  (list :index index :stack stack))

(defun helm-gtags--put-context-stack (tag-location index stack)
  (puthash tag-location (helm-gtags--new-context-info index stack)
           helm-gtags--context-stack))

(defsubst helm-gtags--current-context ()
  (let ((file (buffer-file-name (current-buffer))))
    (list :file file :position (point) :readonly buffer-file-read-only)))

(defsubst helm-gtags--save-current-context ()
  (setq helm-gtags--saved-context (helm-gtags--current-context)))

(defun helm-gtags--open-file (file readonly)
  (if readonly
      (find-file-read-only file)
    (find-file file)))

(defun helm-gtags--open-file-other-window (file readonly)
  (setq helm-gtags--use-otherwin nil)
  (if readonly
      (find-file-read-only-other-window file)
    (find-file-other-window file)))

(defun helm-gtags--get-context-info ()
  (let* ((tag-location (helm-gtags--find-tag-directory))
         (context-info (gethash tag-location helm-gtags--context-stack))
         (context-stack (plist-get context-info :stack)))
    (if (null context-stack)
        (error "Context stack is empty(TAG at %s)" tag-location)
      context-info)))

(defun helm-gtags--get-or-create-context-info ()
  (or (gethash helm-gtags--tag-location helm-gtags--context-stack)
      (helm-gtags--new-context-info -1 nil)))

;;;###autoload
(defun helm-gtags-clear-all-cache ()
  (interactive)
  (clrhash helm-gtags--result-cache))

;;;###autoload
(defun helm-gtags-clear-cache ()
  (interactive)
  (helm-gtags--find-tag-directory)
  (let* ((tag-location (or helm-gtags--real-tag-location
                           helm-gtags--tag-location))
         (gtags-path (concat tag-location "GTAGS"))
         (gpath-path (concat tag-location "GPATH")))
    (remhash gtags-path helm-gtags--result-cache)
    (remhash gpath-path helm-gtags--result-cache)))

(defun helm-gtags--move-to-context (context)
  (let ((file (plist-get context :file))
        (curpoint (plist-get context :position))
        (readonly (plist-get context :readonly)))
    (helm-gtags--open-file file readonly)
    (goto-char curpoint)
    (recenter)))

;;;###autoload
(defun helm-gtags-next-history ()
  "Jump to next position on context stack"
  (interactive)
  (let* ((context-info (helm-gtags--get-context-info))
         (current-index (plist-get context-info :index))
         (context-stack (plist-get context-info :stack))
         context)
    (when (<= current-index -1)
      (error "This context is latest in context stack"))
    (setf (nth current-index context-stack) (helm-gtags--current-context))
    (cl-decf current-index)
    (if (= current-index -1)
        (setq context helm-gtags--current-position
              helm-gtags--current-position nil)
      (setq context (nth current-index context-stack)))
    (helm-gtags--put-context-stack helm-gtags--tag-location
                                   current-index context-stack)
    (helm-gtags--move-to-context context)))

;;;###autoload
(defun helm-gtags-previous-history ()
  "Jump to previous position on context stack"
  (interactive)
  (let* ((context-info (helm-gtags--get-context-info))
         (current-index (plist-get context-info :index))
         (context-stack (plist-get context-info :stack))
         (context-length (length context-stack)))
    (cl-incf current-index)
    (when (>= current-index context-length)
      (error "This context is last in context stack"))
    (if (= current-index 0)
        (setq helm-gtags--current-position (helm-gtags--current-context))
      (setf (nth (- current-index 1) context-stack) (helm-gtags--current-context)))
    (let ((prev-context (nth current-index context-stack)))
      (helm-gtags--move-to-context prev-context))
    (helm-gtags--put-context-stack helm-gtags--tag-location
                                   current-index context-stack)))

(defun helm-gtags--get-result-cache (file)
  (helm-gtags--find-tag-directory)
  (let* ((file-path (concat (or helm-gtags--real-tag-location
                                helm-gtags--tag-location)
                            file))
         (file-mtime (nth 5 (file-attributes file-path)))
         (hash-value (gethash file-path helm-gtags--result-cache))
         (cached-file-mtime (nth 0 hash-value)))
    (if (and cached-file-mtime (equal cached-file-mtime file-mtime))
        (nth 1 hash-value)
      nil)))

(defun helm-gtags--put-result-cache (file cache)
  (helm-gtags--find-tag-directory)
  (let* ((file-path (concat (or helm-gtags--real-tag-location
                                helm-gtags--tag-location)
                            file))
         (file-mtime (nth 5 (file-attributes file-path)))
         (hash-value (list file-mtime cache)))
    (puthash file-path hash-value helm-gtags--result-cache)))

(defun helm-gtags--referer-function (file ref-line)
  (let ((is-opened (cl-loop with path = (concat default-directory file)
                            for buf in (buffer-list)
                            when (string= (buffer-file-name buf) path)
                            return it))
        retval)
    (with-current-buffer (find-file-noselect file)
      (goto-char (point-min))
      (forward-line (1- ref-line))
      (unless (zerop (current-indentation))
        (setq retval (which-function)))
      (unless is-opened
        (kill-buffer (current-buffer)))
      retval)))

(defun helm-gtags--show-detail ()
  (goto-char (point-min))
  (while (not (eobp))
    (let ((line (helm-current-line-contents)))
      (let* ((file-and-line (helm-gtags--extract-file-and-line line))
             (file (car file-and-line))
             (ref-line (cdr file-and-line))
             (ref-func (helm-gtags--referer-function file ref-line)))
        (when ref-func
          (search-forward ":" nil nil 2)
          (insert " " ref-func "|"))
        (forward-line 1)))))

(defun helm-gtags--print-path-in-gtagslibpath (args)
  (let ((libpath (getenv "GTAGSLIBPATH")))
    (when libpath
      (dolist (path (parse-colon-path libpath))
        (let ((default-directory (file-name-as-directory path)))
          (apply #'process-file "global" nil t nil "-Poa" args))))))

(defun helm-gtags--exec-global-command (type input &optional detail)
  (let ((args (helm-gtags--construct-command type input)))
    (helm-gtags--find-tag-directory)
    (helm-gtags--save-current-context)
    (let ((buf-coding buffer-file-coding-system))
      (with-current-buffer (helm-candidate-buffer 'global)
        (let ((default-directory (helm-gtags--base-directory))
              (input (car (last args)))
              (coding-system-for-read buf-coding)
              (coding-system-for-write buf-coding))
          (unless (zerop (apply #'process-file "global" nil '(t nil) nil args))
            (error (format "%s: not found" input)))
          ;; --path options does not support searching under GTAGSLIBPATH
          (when (eq type 'find-file)
            (helm-gtags--print-path-in-gtagslibpath args))
          (helm-gtags--remove-carrige-returns)
          (when detail
            (helm-gtags--show-detail)))))))

(defun helm-gtags--construct-command (type &optional in)
  (setq helm-gtags--local-directory nil)
  (let ((dir (helm-attr 'helm-gtags-base-directory (helm-get-current-source))))
    (when (and dir (not (eq type 'find-file)))
      (setq helm-gtags--local-directory dir)))
  (let ((input (or in helm-gtags--query))
        (options (helm-gtags--construct-options type nil)))
    (when (string= input "")
      (error "Input is empty!!"))
    (setq helm-gtags--last-input input)
    (reverse (cons input options))))

(defun helm-gtags--tags-init (&optional input)
  (helm-gtags--exec-global-command 'tag input))

(defun helm-gtags--pattern-init (&optional input)
  (helm-gtags--exec-global-command 'pattern input helm-gtags-display-style))

(defun helm-gtags--rtags-init (&optional input)
  (helm-gtags--exec-global-command 'rtag input helm-gtags-display-style))

(defun helm-gtags--gsyms-init ()
  (helm-gtags--exec-global-command 'symbol nil helm-gtags-display-style))

(defun helm-gtags--files-init ()
  (helm-gtags--exec-global-command 'find-file nil))

(defun helm-gtags--real-file-name ()
  (let ((buffile (buffer-file-name)))
    (unless buffile
      (error "This buffer is not related to file."))
    (if (file-remote-p buffile)
        (tramp-file-name-localname (tramp-dissect-file-name buffile))
      (file-truename buffile))))

(defun helm-gtags--find-tag-from-here-init ()
  (helm-gtags--find-tag-directory)
  (helm-gtags--save-current-context)
  (let ((token (helm-gtags--token-at-point 'from-here)))
    (unless token
      (error "Cursor is not on symbol."))
    (let* ((filename (helm-gtags--real-file-name))
           (from-here-opt (format "--from-here=%d:%s"
                                  (line-number-at-pos)
                                  (if (eq system-type 'cygwin)
                                      (cygwin-convert-file-name-to-windows filename)
                                    filename))))
      (setq helm-gtags--last-input token)
      (with-current-buffer (helm-candidate-buffer 'global)
        (let* ((default-directory (helm-gtags--base-directory))
               (status (process-file "global" nil '(t nil) nil
                                     "--result=grep" from-here-opt token)))
          (helm-gtags--remove-carrige-returns)
          (unless (zerop status)
            (cond ((= status 1)
                   (error "Error: %s%s" (buffer-string) filename))
                  ((= status 3)
                   (error "Error: %s" (buffer-string)))
                  (t (error "%s: not found" token)))))))))

(defun helm-gtags--parse-file-init ()
  (with-current-buffer (helm-candidate-buffer 'global)
    (unless (zerop (process-file "global" nil t nil
                                 "--result=cscope" "-f" helm-gtags--parsed-file))
      (error "Failed: 'global --result=cscope -f %s" helm-gtags--parsed-file))
    (helm-gtags--remove-carrige-returns)))

(defun helm-gtags--push-context (context)
  (let* ((context-info (helm-gtags--get-or-create-context-info))
         (current-index (plist-get context-info :index))
         (context-stack (plist-get context-info :stack)))
    (unless (= current-index -1)
      (setq context-stack (nthcdr (1+ current-index) context-stack)))
    (setq helm-gtags--current-position nil)
    (push context context-stack)
    (helm-gtags--put-context-stack helm-gtags--tag-location -1 context-stack)))

(defsubst helm-gtags--select-find-file-func ()
  (if helm-gtags--use-otherwin
      #'helm-gtags--open-file-other-window
    #'helm-gtags--open-file))

(defun helm-gtags--do-open-file (open-func file line)
  (funcall open-func file helm-gtags-read-only)
  (goto-char (point-min))
  (forward-line (1- line))
  (back-to-indentation)
  (recenter)
  (helm-gtags--push-context helm-gtags--saved-context)
  (when helm-gtags-pulse-at-cursor
    (pulse-momentary-highlight-one-line (point))))

(defun helm-gtags--find-line-number (cand)
  (if (string-match "\\s-+\\([1-9][0-9]+\\)\\s-+" cand)
      (string-to-number (match-string-no-properties 1 cand))
    (error "Can't find line number in %s" cand)))

(defun helm-gtags--parse-file-action (cand)
  (let ((line (helm-gtags--find-line-number cand))
        (open-func (helm-gtags--select-find-file-func)))
    (helm-gtags--do-open-file open-func helm-gtags--parsed-file line)))

(defsubst helm-gtags--has-drive-letter-p (path)
  (string-match-p "\\`[a-zA-Z]:" path))

(defun helm-gtags--extract-file-and-line (cand)
  (if (and (helm-gtags--windows-p) (helm-gtags--has-drive-letter-p cand))
      (when (string-match "\\(\\`[a-zA-Z]:[^:]+\\):\\([^:]+\\)" cand)
        (cons (match-string-no-properties 1 cand)
              (string-to-number (match-string-no-properties 2 cand))))
    (let ((elems (split-string cand ":")))
      (cons (cl-first elems) (string-to-number (cl-second elems))))))

(defun helm-gtags--action-openfile (cand)
  (let* ((file-and-line (helm-gtags--extract-file-and-line cand))
         (filename (car file-and-line))
         (line (cdr file-and-line))
         (open-func (helm-gtags--select-find-file-func))
         (default-directory (helm-gtags--base-directory)))
    (helm-gtags--do-open-file open-func filename line)))

(defun helm-gtags--action-openfile-other-window (cand)
  (let ((helm-gtags--use-otherwin t))
    (helm-gtags--action-openfile cand)))

(defun helm-gtags--file-content-at-pos (file pos)
  (with-current-buffer (find-file-noselect file)
    (save-excursion
      (goto-char pos)
      (format "%s:%d:%s:%s"
              file (line-number-at-pos)
              (helm-aif (which-function) (format "[%s]" it) "")
              (helm-current-line-contents)))))

(defun helm-gtags--files-candidate-transformer (file)
  (if (eq helm-gtags-path-style 'absolute)
      file
    (let ((removed-regexp (concat "\\`" helm-gtags--tag-location)))
      (replace-regexp-in-string removed-regexp "" file))))

(defun helm-gtags--show-stack-init ()
  (cl-loop with context-stack = (plist-get (helm-gtags--get-context-info) :stack)
           with stack-length = (length context-stack)
           for context in (reverse context-stack)
           for file = (plist-get context :file)
           for pos  = (plist-get context :position)
           for index = (1- stack-length) then (1- index)
           for line = (helm-gtags--file-content-at-pos file pos)
           for cand = (helm-gtags--files-candidate-transformer line)
           collect (cons cand (propertize cand 'index index))))

(defun helm-gtags--persistent-action (cand)
  (let* ((file-and-line (helm-gtags--extract-file-and-line cand))
         (filename (car file-and-line))
         (line (cdr file-and-line))
         (default-directory (helm-gtags--base-directory)))
    (when (eq helm-gtags-path-style 'relative)
      (setq helm-gtags--last-default-directory default-directory))
    (find-file filename)
    (goto-char (point-min))
    (forward-line (1- line))
    (helm-highlight-current-line)))

(defvar helm-gtags--find-file-action
  (helm-make-actions
   "Open file" #'helm-gtags--action-openfile
   "Open file other window" #'helm-gtags--action-openfile-other-window))

(defvar helm-source-gtags-tags
  (helm-build-in-buffer-source "Jump to definitions"
    :init 'helm-gtags--tags-init
    :candidate-number-limit helm-gtags-maximum-candidates
    :real-to-display 'helm-gtags--candidate-transformer
    :persistent-action 'helm-gtags--persistent-action
    :fuzzy-match helm-gtags-fuzzy-match
    :action helm-gtags--find-file-action))

(defvar helm-source-gtags-pattern
  (helm-build-in-buffer-source "Find pattern"
    :init 'helm-gtags--pattern-init
    :candidate-number-limit helm-gtags-maximum-candidates
    :real-to-display 'helm-gtags--candidate-transformer
    :persistent-action 'helm-gtags--persistent-action
    :fuzzy-match helm-gtags-fuzzy-match
    :action helm-gtags--find-file-action))

(defvar helm-source-gtags-rtags
  (helm-build-in-buffer-source "Jump to references"
    :init 'helm-gtags--rtags-init
    :candidate-number-limit helm-gtags-maximum-candidates
    :real-to-display 'helm-gtags--candidate-transformer
    :persistent-action 'helm-gtags--persistent-action
    :fuzzy-match helm-gtags-fuzzy-match
    :action helm-gtags--find-file-action))

(defvar helm-source-gtags-gsyms
  (helm-build-in-buffer-source "Jump to symbols"
    :init 'helm-gtags--gsyms-init
    :candidate-number-limit helm-gtags-maximum-candidates
    :real-to-display 'helm-gtags--candidate-transformer
    :persistent-action 'helm-gtags--persistent-action
    :fuzzy-match helm-gtags-fuzzy-match
    :action helm-gtags--find-file-action))

(defun helm-gtags--highlight-candidate (candidate)
  (let ((regexp (concat "\\_<" helm-gtags--last-input "\\_>"))
        (limit (1- (length candidate)))
        (last-pos 0)
        (case-fold-search nil))
    (while (and (< last-pos limit)
                (string-match regexp candidate last-pos))
      (put-text-property (match-beginning 0) (match-end 0)
                         'face 'helm-gtags-match
                         candidate)
      (setq last-pos (1+ (match-end 0))))
    candidate))

(defun helm-gtags--transformer-regexp (candidate)
  (if (and (helm-gtags--windows-p) (helm-gtags--has-drive-letter-p candidate))
      "\\`\\([a-zA-Z]:[^:]+\\):\\([^:]+\\):\\(.*\\)"
    "\\`\\([^:]+\\):\\([^:]+\\):\\(.*\\)"))

(defun helm-gtags--candidate-transformer (candidate)
  (if (not helm-gtags-highlight-candidate)
      candidate
    (let ((regexp (helm-gtags--transformer-regexp candidate)))
      (when (string-match regexp candidate)
        (format "%s:%s:%s"
                (propertize (match-string 1 candidate) 'face 'helm-gtags-file)
                (propertize (match-string 2 candidate) 'face 'helm-gtags-lineno)
                (helm-gtags--highlight-candidate (match-string 3 candidate)))))))

(defun helm-gtags--parse-file-candidate-transformer (file)
  (let ((removed-file (replace-regexp-in-string "\\`\\S-+ " "" file)))
    (when (string-match "\\`\\(\\S-+\\) \\(\\S-+\\) \\(.+\\)\\'" removed-file)
      (format "%-25s %-5s %s"
              (match-string-no-properties 1 removed-file)
              (match-string-no-properties 2 removed-file)
              (match-string-no-properties 3 removed-file)))))

(defvar helm-source-gtags-find-tag-from-here
  (helm-build-in-buffer-source "Find tag from here"
    :init 'helm-gtags--find-tag-from-here-init
    :candidate-number-limit helm-gtags-maximum-candidates
    :real-to-display 'helm-gtags--candidate-transformer
    :persistent-action 'helm-gtags--persistent-action
    :fuzzy-match helm-gtags-fuzzy-match
    :action helm-gtags--find-file-action))

(defvar helm-source-gtags-parse-file
  (helm-build-in-buffer-source "Parse file"
    :init 'helm-gtags--parse-file-init
    :candidate-number-limit helm-gtags-maximum-candidates
    :real-to-display 'helm-gtags--parse-file-candidate-transformer
    :fuzzy-match helm-gtags-fuzzy-match
    :action 'helm-gtags--parse-file-action))

(defun helm-gtags--show-stack-action (cand)
  (let* ((index (get-text-property 0 'index cand))
         (context-info (helm-gtags--get-context-info))
         (context-stack (plist-get context-info :stack)))
    (helm-gtags--put-context-stack helm-gtags--tag-location
                                   index context-stack)
    (helm-gtags--move-to-context (nth index context-stack))))

(defvar helm-source-gtags-show-stack
  (helm-build-sync-source "Show Context Stack"
    :candidates 'helm-gtags--show-stack-init
    :volatile t
    :candidate-number-limit helm-gtags-maximum-candidates
    :persistent-action 'helm-gtags--persistent-action
    :fuzzy-match helm-gtags-fuzzy-match
    :action 'helm-gtags--show-stack-action))

;;;###autoload
(defun helm-gtags-select ()
  (interactive)
  (helm-gtags--common '(helm-source-gtags-select) nil))

;;;###autoload
(defun helm-gtags-select-path ()
  (interactive)
  (helm-gtags--common '(helm-source-gtags-select-path) nil))

(defsubst helm-gtags--beginning-of-defun ()
  (cl-case major-mode
    ((c-mode c++-mode java-mode) 'c-beginning-of-defun)
    (php-mode 'php-beginning-of-defun)
    (otherwise #'beginning-of-defun)))

(defsubst helm-gtags--end-of-defun ()
  (cl-case major-mode
    ((c-mode c++-mode java-mode malabar-mode) 'c-end-of-defun)
    (php-mode 'php-end-of-defun)
    (otherwise #'end-of-defun)))

(defun helm-gtags--current-funcion-bound ()
  (save-excursion
    (let (start)
      (funcall (helm-gtags--beginning-of-defun))
      (setq start (line-number-at-pos))
      (funcall (helm-gtags--end-of-defun))
      (cons start (line-number-at-pos)))))

(defun helm-gtags--tags-refered-from-this-function ()
  (let* ((file (helm-gtags--real-file-name))
         (bound (helm-gtags--current-funcion-bound))
         (start-line (car bound))
         (end-line (cdr bound)))
    (with-temp-buffer
      (unless (process-file "global" nil t nil "-f" "-r" file)
        (error "Failed: global -f -r %s" file))
      (goto-char (point-min))
      (let (tagnames finish)
        (while (and (not finish) (not (eobp)))
          (let* ((cols (split-string (helm-current-line-contents) nil t))
                 (lineno (string-to-number (cl-second cols))))
            (if (and (> lineno start-line) (< lineno end-line))
                (let* ((tag (cl-first cols))
                       (elm (cl-find tag tagnames :test 'equal)))
                  (unless elm
                    (push tag tagnames)))
              (when (>= lineno end-line)
                (setq finish t)))
            (forward-line 1)))
        (reverse tagnames)))))

(defun helm-gtags--tag-in-function-persistent-action (cand)
  (let* ((bound (helm-gtags--current-funcion-bound))
         (limit (save-excursion
                  (goto-char (point-min))
                  (forward-line (cdr bound))
                  (goto-char (line-end-position))
                  (point))))
    (when (search-forward cand nil limit)
      (helm-highlight-current-line))))

;;;###autoload
(defun helm-gtags-tags-in-this-function ()
  "Show tagnames which are referenced in this function and jump to it."
  (interactive)
  (let ((tags (helm-gtags--tags-refered-from-this-function)))
    (unless tags
      (error "There are no tags which are refered from this function."))
    (let* ((name (format "Tags in [%s]" (which-function)))
           (tag (helm-comp-read
                 "Tagnames: " tags
                 :must-match t :name name
                 :persistent-action 'helm-gtags--tag-in-function-persistent-action)))
      (helm-gtags-find-tag tag))))

(defun helm-gtags--source-select-tag (candidate)
  (helm-build-in-buffer-source "Select Tag"
    :init (lambda () (helm-gtags--tags-init candidate))
    :candidate-number-limit helm-gtags-maximum-candidates
    :persistent-action 'helm-gtags--persistent-action
    :fuzzy-match helm-gtags-fuzzy-match
    :action helm-gtags--find-file-action))

(defun helm-gtags--source-select-rtag (candidate)
  (helm-build-in-buffer-source "Select Rtag"
    :init (lambda () (helm-gtags--rtags-init candidate))
    :candidate-number-limit helm-gtags-maximum-candidates
    :persistent-action 'helm-gtags--persistent-action
    :fuzzy-match helm-gtags-fuzzy-match
    :action helm-gtags--find-file-action))

(defsubst helm-gtags--action-by-timer (src)
  (run-with-timer 0.1 nil (lambda () (helm-gtags--common (list src) nil))))

(defun helm-gtags--select-tag-action (c)
  (helm-gtags--action-by-timer (helm-gtags--source-select-tag c)))

(defun helm-gtags--select-rtag-action (c)
  (helm-gtags--action-by-timer (helm-gtags--source-select-rtag c)))

(defun helm-gtags--select-cache-init-common (args tagfile)
  (let ((cache (helm-gtags--get-result-cache tagfile)))
    (if cache
        (insert cache)
      (apply #'process-file "global" nil t nil args)
      (let* ((cache (buffer-string))
             (cache-size (length cache)))
        (when (<= cache-size helm-gtags-cache-max-result-size)
          (helm-gtags--put-result-cache tagfile cache))))))

(defun helm-gtags--source-select-init ()
  (with-current-buffer (helm-candidate-buffer 'global)
    (if (not helm-gtags-cache-select-result)
        (progn
          (process-file "global" nil t nil "-c")
          (helm-gtags--remove-carrige-returns))
      (helm-gtags--select-cache-init-common '("-c") "GTAGS"))))

(defvar helm-source-gtags-select
  (helm-build-in-buffer-source "Find tag from here"
    :init 'helm-gtags--source-select-init
    :candidate-number-limit helm-gtags-maximum-candidates
    :persistent-action #'ignore
    :fuzzy-match helm-gtags-fuzzy-match
    :action (helm-make-actions
             "Goto the location" #'helm-gtags--select-tag-action
             "Goto the location(other buffer)"
             (lambda (c)
               (setq helm-gtags--use-otherwin t)
               (helm-gtags--select-tag-action c))
             "Move to the referenced point" #'helm-gtags--select-rtag-action)))

(defun helm-gtags--select-path-init ()
  (helm-gtags--find-tag-directory)
  (with-current-buffer (helm-candidate-buffer 'global)
    (let ((options (if (eq helm-gtags-path-style 'relative) "-Po" "-Poa")))
      (if (not helm-gtags-cache-select-result)
          (progn
            (process-file "global" nil t nil options)
            (helm-gtags--remove-carrige-returns))
        (helm-gtags--select-cache-init-common (list options) "GPATH")))))

(defun helm-gtags--file-name (name)
  (let ((remote (file-remote-p default-directory)))
    (if (not remote)
        name
      (cl-case helm-gtags-path-style
        (relative name)
        (otherwise (concat remote name))))))

(defun helm-gtags--find-file-common (open-fn cand)
  (let ((default-directory (helm-gtags--base-directory)))
    (funcall open-fn (helm-gtags--file-name cand))))

(defun helm-gtags--find-file (cand)
  (helm-gtags--find-file-common #'find-file cand))

(defun helm-gtags--find-file-other-window (cand)
  (helm-gtags--find-file-common #'find-file-other-window cand))

(defvar helm-gtags--file-util-action
  (helm-make-actions
   "Open file" #'helm-gtags--find-file
   "Open file other window" #'helm-gtags--find-file-other-window))

(defun helm-gtags--file-persistent-action (cand)
  (let ((default-directory (with-helm-current-buffer
                             default-directory)))
    (helm-ff-kill-or-find-buffer-fname (helm-gtags--file-name cand))))

(defvar helm-source-gtags-select-path
  (helm-build-in-buffer-source "Select path"
    :init 'helm-gtags--select-path-init
    :candidate-number-limit helm-gtags-maximum-candidates
    :real-to-display 'helm-gtags--files-candidate-transformer
    :persistent-action #'helm-gtags--file-persistent-action
    :fuzzy-match helm-gtags-fuzzy-match
    :action helm-gtags--file-util-action))

(defun helm-gtags--searched-directory ()
  (cl-case (prefix-numeric-value current-prefix-arg)
    (4 (let ((dir (read-directory-name "Input Directory: ")))
         (setq helm-gtags--local-directory (file-name-as-directory dir))))
    (16 (file-name-directory (buffer-file-name)))))

(defsubst helm-gtags--using-other-window-p ()
  (< (prefix-numeric-value current-prefix-arg) 0))

(defun helm-gtags--make-gtags-sentinel (action)
  (lambda (process _event)
    (when (eq (process-status process) 'exit)
      (if (zerop (process-exit-status process))
          (message "Success: %s TAGS" action)
        (message "Failed: %s TAGS(%d)" action (process-exit-status process))))))

(defsubst helm-gtags--read-gtagslabel ()
  (let ((labels '("default" "native" "ctags" "pygments")))
    (completing-read "GTAGSLABEL(Default: default): " labels nil t nil nil "default")))

(defsubst helm-gtags--label-option (label)
  (concat "--gtagslabel=" label))

;;;###autoload
(defun helm-gtags-create-tags (dir label)
  (interactive
   (list (read-directory-name "Root Directory: ")
         (helm-gtags--read-gtagslabel)))
  (let ((default-directory dir)
        (proc-buf (get-buffer-create " *helm-gtags-create*")))
    (let ((proc (start-file-process "helm-gtags-create" proc-buf
                                    "gtags" "-q" (helm-gtags--label-option label))))
      (set-process-sentinel proc (helm-gtags--make-gtags-sentinel 'create)))))

(defun helm-gtags--find-tag-simple ()
  (or (getenv "GTAGSROOT")
      (locate-dominating-file default-directory "GTAGS")
      (if (not (yes-or-no-p "File GTAGS not found. Run 'gtags'? "))
          (user-error "Abort")
        (let* ((tagroot (read-directory-name "Root Directory: "))
               (label (helm-gtags--read-gtagslabel))
               (default-directory tagroot))
          (message "gtags is generating tags....")
          (unless (zerop (process-file "gtags" nil nil nil
                                       "-q" (helm-gtags--label-option label)))
            (error "Faild: 'gtags -q'"))
          tagroot))))

(defun helm-gtags--current-file-and-line ()
  (let* ((buffile (buffer-file-name))
         (path (cl-case helm-gtags-path-style
                 (absolute buffile)
                 (root
                  (file-relative-name buffile (helm-gtags--find-tag-directory)))
                 (relative
                  (file-relative-name buffile (helm-gtags--base-directory))))))
    (format "%s:%d" path (line-number-at-pos))))

(defsubst helm-gtags--clear-variables ()
  (setq helm-gtags--last-default-directory nil))

(defun helm-gtags--common (srcs tagname)
  (helm-gtags--clear-variables)
  (let ((helm-quit-if-no-candidate t)
        (helm-execute-action-at-once-if-one t)
        (dir (helm-gtags--searched-directory))
        (src (car srcs))
        (preselect-regexp (when helm-gtags-preselect
                            (regexp-quote (helm-gtags--current-file-and-line)))))
    (when (symbolp src)
      (setq src (symbol-value src)))
    (unless helm-gtags--use-otherwin
      (setq helm-gtags--use-otherwin (helm-gtags--using-other-window-p)))
    (when tagname
      (setq helm-gtags--query tagname))
    (let ((tagroot (helm-gtags--find-tag-simple)))
      (helm-attrset 'helm-gtags-base-directory dir src)
      (when tagname
        (helm-attrset 'name (format "%s in %s" tagname (or dir tagroot)) src))
      (helm :sources srcs :buffer helm-gtags--buffer
            :preselect preselect-regexp))))

;;;###autoload
(defun helm-gtags-find-tag (tag)
  "Jump to definition"
  (interactive
   (list (helm-gtags--read-tagname 'tag)))
  (helm-gtags--common '(helm-source-gtags-tags) tag))

;;;###autoload
(defun helm-gtags-find-tag-other-window (tag)
  "Jump to definition in other window."
  (interactive
   (list (helm-gtags--read-tagname 'tag)))
  (setq helm-gtags--use-otherwin t)
  (helm-gtags-find-tag tag))

;;;###autoload
(defun helm-gtags-find-rtag (tag)
  "Jump to referenced point"
  (interactive
   (list (helm-gtags--read-tagname 'rtag (which-function))))
  (helm-gtags--common '(helm-source-gtags-rtags) tag))

;;;###autoload
(defun helm-gtags-find-symbol (tag)
  "Jump to the symbol location"
  (interactive
   (list (helm-gtags--read-tagname 'symbol)))
  (helm-gtags--common '(helm-source-gtags-gsyms) tag))

;;;###autoload
(defun helm-gtags-find-pattern (pattern)
  "Grep and jump by gtags tag files."
  (interactive
   (list (helm-gtags--read-tagname 'pattern)))
  (helm-gtags--common '(helm-source-gtags-pattern) pattern))

(defun helm-gtags--find-file-after-hook ()
  (helm-gtags--push-context helm-gtags--saved-context))

(defvar helm-source-gtags-files
  (helm-build-in-buffer-source "Find files"
    :init #'helm-gtags--files-init
    :candidate-number-limit helm-gtags-maximum-candidates
    :real-to-display #'helm-gtags--files-candidate-transformer
    :persistent-action #'helm-gtags--file-persistent-action
    :fuzzy-match helm-gtags-fuzzy-match
    :action helm-gtags--file-util-action))

;;;###autoload
(defun helm-gtags-find-files (file)
  "Find file from tagged with gnu global."
  (interactive
   (list (helm-gtags--read-tagname 'find-file)))
  (add-hook 'helm-after-action-hook 'helm-gtags--find-file-after-hook)
  (unwind-protect
      (helm-gtags--common '(helm-source-gtags-files) file)
    (remove-hook 'helm-after-action-hook 'helm-gtags--find-file-after-hook)))

;;;###autoload
(defun helm-gtags-find-tag-from-here ()
  "Jump point by current point information.
Jump to definition point if cursor is on its reference.
Jump to reference point if curosr is on its definition"
  (interactive)
  (helm-gtags--common '(helm-source-gtags-find-tag-from-here) nil))

;;;###autoload
(defun helm-gtags-dwim ()
  "Find by context. Here is
- on include statement then jump to included file
- on symbol definition then jump to its references
- on reference point then jump to its definition."
  (interactive)
  (let ((line (helm-current-line-contents)))
    (if (string-match helm-gtags--include-regexp line)
        (let ((helm-gtags-use-input-at-cursor t))
          (helm-gtags-find-files (match-string-no-properties 1 line)))
      (if (and (buffer-file-name) (thing-at-point 'symbol))
          (helm-gtags-find-tag-from-here)
        (call-interactively 'helm-gtags-find-tag)))))

(defun helm-gtags--set-parsed-file ()
  (let* ((this-file (file-name-nondirectory (buffer-file-name)))
         (file (if current-prefix-arg
                   (read-file-name "Parsed File: " nil this-file)
                 this-file)))
    (setq helm-gtags--parsed-file (expand-file-name file))))

(defun helm-gtags--find-preselect-line ()
  (let ((defun-bound (bounds-of-thing-at-point 'defun)))
    (if (not defun-bound)
        (line-number-at-pos)
      (let ((defun-begin-line (line-number-at-pos (car defun-bound)))
            (filename (helm-gtags--real-file-name)))
        (with-temp-buffer
          (unless (zerop (process-file "global" nil t nil "-f" filename))
            (error "Failed: global -f"))
          (goto-char (point-min))
          (let (start-line)
            (while (and (not start-line)
                        (re-search-forward "^\\S-+\\s-+\\([1-9][0-9]*\\)" nil t))
              (let ((line (string-to-number (match-string-no-properties 1))))
                (when (>= line defun-begin-line)
                  (setq start-line line))))
            (or start-line (line-number-at-pos))))))))

;;;###autoload
(defun helm-gtags-parse-file ()
  "Parse current file with gnu global. This is similar to `imenu'.
You can jump definitions of functions, symbols in this file."
  (interactive)
  (helm-gtags--find-tag-directory)
  (helm-gtags--save-current-context)
  (setq helm-gtags--use-otherwin (helm-gtags--using-other-window-p))
  (helm-gtags--set-parsed-file)
  (helm-attrset 'name
                (format "Parsed File: %s"
                        (file-relative-name helm-gtags--parsed-file
                                            helm-gtags--tag-location))
                helm-source-gtags-parse-file)
  (let ((presel (when helm-gtags-preselect
                  (format "^\\S-+\\s-+%d\\s-+" (helm-gtags--find-preselect-line)))))
    (helm :sources '(helm-source-gtags-parse-file)
          :buffer helm-gtags--buffer :preselect presel)))

;;;###autoload
(defun helm-gtags-pop-stack ()
  "Jump to previous point on the context stack and pop it from stack."
  (interactive)
  (let* ((context-info (helm-gtags--get-context-info))
         (context-stack (plist-get context-info :stack))
         (context (pop context-stack)))
    (helm-gtags--put-context-stack helm-gtags--tag-location -1 context-stack)
    (helm-gtags--move-to-context context)))

;;;###autoload
(defun helm-gtags-show-stack ()
  "Show current context stack."
  (interactive)
  (helm-other-buffer 'helm-source-gtags-show-stack
                     (get-buffer-create helm-gtags--buffer)))

;;;###autoload
(defun helm-gtags-clear-stack ()
  "Clear current context stack."
  (interactive)
  (let ((tag-location (helm-gtags--find-tag-directory)))
    (message "Clear '%s' context stack." tag-location)
    (remhash tag-location helm-gtags--context-stack)))

;;;###autoload
(defun helm-gtags-clear-all-stacks ()
  "Clear all context stacks."
  (interactive)
  (message "Clear all context statks.")
  (setq helm-gtags--context-stack (make-hash-table :test 'equal)))

(defun helm-gtags--read-tag-directory ()
  (let ((dir (read-directory-name "Directory tag generated: " nil nil t)))
    ;; On Windows, "gtags d:/tmp" work, but "gtags d:/tmp/" doesn't
    (directory-file-name (expand-file-name dir))))

(defsubst helm-gtags--how-to-update-tags ()
  (cl-case (prefix-numeric-value current-prefix-arg)
    (4 'entire-update)
    (16 'generate-other-directory)
    (otherwise 'single-update)))

(defun helm-gtags--update-tags-command (how-to)
  (cl-case how-to
    (entire-update '("global" "-u"))
    (generate-other-directory (list "gtags" (helm-gtags--read-tag-directory)))
    (single-update (list "global" "--single-update" (helm-gtags--real-file-name)))))

(defun helm-gtags--update-tags-p (how-to interactive-p current-time)
  (or interactive-p
      (and (eq how-to 'single-update)
           (buffer-file-name)
           (or (not helm-gtags-update-interval-second)
               (>= (- current-time helm-gtags--last-update-time)
                   helm-gtags-update-interval-second)))))

;;;###autoload
(defun helm-gtags-update-tags ()
  "Update TAG file. Update All files with `C-u' prefix.
Generate new TAG file in selected directory with `C-u C-u'"
  (interactive)
  (let ((how-to (helm-gtags--how-to-update-tags))
        (interactive-p (called-interactively-p 'interactive))
        (current-time (float-time (current-time))))
    (when (helm-gtags--update-tags-p how-to interactive-p current-time)
      (let* ((cmds (helm-gtags--update-tags-command how-to))
             (proc (apply #'start-file-process "helm-gtags-update-tag" nil cmds)))
        (if (not proc)
            (message "Failed: %s" (mapconcat 'identity cmds " "))
          (set-process-sentinel proc (helm-gtags--make-gtags-sentinel 'update))
          (setq helm-gtags--last-update-time current-time))))))

;;;###autoload
(defun helm-gtags-resume ()
  "Resurrect previously invoked `helm-gtags` command."
  (interactive)
  (unless (get-buffer helm-gtags--buffer)
    (error "Error: helm-gtags buffer is not existed."))
  (helm-resume helm-gtags--buffer))

(defsubst helm-gtags--check-browser-installed (browser)
  (let ((used-browser (or browser "mozilla")))
    (unless (executable-find used-browser)
      (error "Not found browser '%s'" used-browser))))

(defun helm-gtags-display-browser ()
  "Display current screen on hypertext browser.
`browse-url-generic-program' is used as browser if its value is non-nil.
`mozilla' is used in other case."
  (interactive)
  (let ((file (buffer-file-name)))
    (if (not file)
        (error "This buffer is not related to file.")
      (let* ((lineopt (concat "+" (number-to-string (line-number-at-pos))))
             (browser (symbol-value 'browse-url-generic-program))
             (args (list lineopt file)))
        (helm-gtags--check-browser-installed browser)
        (when browser
          (setq args (append (list "-b" browser) args)))
        ;; `gozilla' commend never returns error status if command is failed.
        (apply #'process-file "gozilla" nil nil nil args)))))

(defvar helm-gtags-mode-name " HelmGtags")
(defvar helm-gtags-mode-map (make-sparse-keymap))

;;;###autoload
(define-minor-mode helm-gtags-mode ()
  "Enable helm-gtags"
  :init-value nil
  :global     nil
  :keymap     helm-gtags-mode-map
  :lighter    helm-gtags-mode-name
  (if helm-gtags-mode
      (when helm-gtags-auto-update
        (add-hook 'after-save-hook 'helm-gtags-update-tags nil t))
    (when helm-gtags-auto-update
      (remove-hook 'after-save-hook 'helm-gtags-update-tags t))))

;; Key mapping of gtags-mode.
(when helm-gtags-suggested-key-mapping
  ;; Current key mapping.
  (let ((command-table '(("h" . helm-gtags-display-browser)
                         ("P" . helm-gtags-find-files)
                         ("f" . helm-gtags-parse-file)
                         ("g" . helm-gtags-find-pattern)
                         ("s" . helm-gtags-find-symbol)
                         ("r" . helm-gtags-find-rtag)
                         ("t" . helm-gtags-find-tag)
                         ("d" . helm-gtags-find-tag)))
        (key-func (if (string-prefix-p "\\" helm-gtags-prefix-key)
                      #'concat
                    (lambda (prefix key) (kbd (concat prefix " " key))))))
    (cl-loop for (key . command) in command-table
             do
             (define-key helm-gtags-mode-map (funcall key-func helm-gtags-prefix-key key) command))

    ;; common
    (define-key helm-gtags-mode-map "\C-]" 'helm-gtags-find-tag-from-here)
    (define-key helm-gtags-mode-map "\C-t" 'helm-gtags-pop-stack)
    (define-key helm-gtags-mode-map "\e*" 'helm-gtags-pop-stack)
    (define-key helm-gtags-mode-map "\e." 'helm-gtags-find-tag)
    (define-key helm-gtags-mode-map "\C-x4." 'helm-gtags-find-tag-other-window)))

(provide 'helm-gtags)

;; Local Variables:
;; coding: utf-8
;; indent-tabs-mode: nil
;; End:

;;; helm-gtags.el ends here
