;;; goto-char-preview.el --- Preview character when executing `goto-char` command  -*- lexical-binding: t; -*-

;; Copyright (C) 2019-2026 Shen, Jen-Chieh
;; Created date 2019-04-18 16:03:46

;; Author: Shen, Jen-Chieh <jcs090218@gmail.com>
;; URL: https://github.com/emacs-vs/goto-char-preview
;; Package-Version: 0.2.0
;; Package-Revision: 13c9ab10b0a4
;; Package-Requires: ((emacs "24.3"))
;; Keywords: convenience character navigation

;; 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:
;;
;; Preview character when executing `goto-char` command.
;;

;;; Code:

(require 'isearch)

(defgroup goto-char-preview nil
  "Preview char when executing `goto-char` command."
  :prefix "goto-char-preview-"
  :group 'convenience
  :group 'tools
  :link '(url-link :tag "Repository" "https://github.com/emacs-vs/goto-char-preview"))

(defcustom goto-char-preview-before-hook nil
  "Hooks run before `goto-char-preview' is run."
  :group 'goto-char-preview
  :type 'hook)

(defcustom goto-char-preview-after-hook nil
  "Hooks run after `goto-char-preview' is run."
  :group 'goto-char-preview
  :type 'hook)

(defcustom goto-char-preview-hl-duration 1
  "Duration of highlight when change preview char."
  :group 'goto-char-preview
  :type 'integer)

(defface goto-char-preview-hl
  '((t :inherit highlight :extend t))
  "Face to use for highlighting when change preview char."
  :group 'goto-char-preview)

(defvar goto-char-preview--prev-window nil
  "Record down the previous window before we do preview command.")

(defvar goto-char-preview--prev-char-pos nil
  "Record down the previous character position before we do preview command.")

(defvar goto-char-preview--relative-p nil
  "Flag to see if this command relative.")

(defvar goto-char-preview--executing-p nil
  "Set to t when the command is getting executed.")

(defun goto-char-preview--highlight ()
  "Keep highlight for a fixed time."
  (when goto-char-preview-hl-duration
    (let ((overlay (make-overlay (line-beginning-position) (1+ (line-end-position)))))
      (overlay-put overlay 'face 'goto-char-preview-hl)
      (overlay-put overlay 'window (selected-window))
      (sit-for goto-char-preview-hl-duration)
      (delete-overlay overlay))))

(defun goto-char-preview--do (char-pos)
  "Do goto char.
CHAR-POS : Target character position to navigate to."
  (save-selected-window
    (select-window goto-char-preview--prev-window)
    (goto-char (point-min))
    (when (< (point-max) char-pos)
      (setq char-pos (point-max)))
    (forward-char (1- char-pos))
    (ignore-errors
      (let ((search-invisible 'open))
        (isearch-range-invisible (1- char-pos)
                                 char-pos)))
    (goto-char-preview--highlight)))

(defun goto-char-preview--do-preview ()
  "Do the goto char preview action."
  (when (and goto-char-preview--executing-p
             goto-char-preview--prev-window)
    (save-selected-window
      (let ((char-pos-str (thing-at-point 'line)))
        (select-window goto-char-preview--prev-window)
        (if char-pos-str
            (let ((char-pos (string-to-number char-pos-str)))
              (when (<= char-pos 0) (setq char-pos 1))
              (when goto-char-preview--relative-p
                (setq char-pos (+ goto-char-preview--prev-char-pos char-pos)))
              (goto-char-preview--do char-pos))
          (goto-char-preview--do goto-char-preview--prev-char-pos))))))

;;;###autoload
(defun goto-char-preview ()
  "Preview goto char."
  (interactive)
  (let ((goto-char-preview--executing-p t)
        (goto-char-preview--prev-window (selected-window))
        (window-point (window-point))
        (goto-char-preview--prev-char-pos (point))
        jumped)
    (run-hooks 'goto-char-preview-before-hook)
    (unwind-protect
        (setq jumped (read-number
                      (format (if goto-char-preview--relative-p
                                  "[%d] Goto char relative: (%d to %d) "
                                "[%d] Goto char: (%d to %d) ")
                              goto-char-preview--prev-char-pos
                              (point-min)
                              (point-max))))
      (if jumped
          (with-current-buffer (window-buffer goto-char-preview--prev-window)
            (unless (region-active-p) (push-mark window-point)))
        (set-window-point goto-char-preview--prev-window window-point))
      (run-hooks 'goto-char-preview-after-hook))))

;;;###autoload
(defun goto-char-preview-relative ()
  "Preview goto char relative."
  (interactive)
  (let ((goto-char-preview--relative-p t))
    (goto-char-preview)))

(defun goto-char-preview--minibuffer-setup ()
  "Locally set up preview hooks for this minibuffer command."
  (add-hook 'post-command-hook #'goto-char-preview--do-preview nil t))

(add-hook 'minibuffer-setup-hook 'goto-char-preview--minibuffer-setup)

(provide 'goto-char-preview)
;;; goto-char-preview.el ends here
