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

;; Copyright (C) 2019-2026 Shen, Jen-Chieh
;; Created date 2019-03-01 14:53:00

;; Author: Shen, Jen-Chieh <jcs090218@gmail.com>
;; URL: https://github.com/emacs-vs/goto-line-preview
;; Package-Version: 0.2.0
;; Package-Revision: f835428addba
;; Package-Requires: ((emacs "25"))
;; Keywords: convenience line 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 line when executing `goto-line` command.
;;

;;; Code:

(require 'isearch)

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

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

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

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

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

(defvar goto-line-preview--prev-window nil
  "Record down the previous window before we do preivew display.")

(defvar goto-line-preview--prev-line-num nil
  "Record down the previous line number before we do preivew display.")

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

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

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

(defun goto-line-preview--do (line-num)
  "Do goto LINE-NUM."
  (save-selected-window
    (select-window goto-line-preview--prev-window)
    (goto-char (point-min))
    (forward-line (1- line-num))
    (ignore-errors
      (let ((search-invisible 'open))
        (isearch-range-invisible (1+ (line-beginning-position))
                                 (+ (line-beginning-position) 2))))
    (goto-line-preview--highlight)))

(defun goto-line-preview--do-preview ()
  "Do the goto line preview action."
  (when (and goto-line-preview--executing-p
             goto-line-preview--prev-window)
    (save-selected-window
      (let ((line-num-str (thing-at-point 'line)))
        (select-window goto-line-preview--prev-window)
        (if line-num-str
            (let ((line-num (string-to-number line-num-str)))
              (when goto-line-preview--relative-p
                (setq line-num (+ goto-line-preview--prev-line-num line-num)))
              (unless (zerop line-num) (goto-line-preview--do line-num)))
          (goto-line-preview--do goto-line-preview--prev-line-num))))))

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

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

;;;###autoload
(define-obsolete-function-alias 'goto-line-preview-goto-line 'goto-line-preview "0.1.1")

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

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

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