;;; lsp-inline-completion.el --- LSP mode                              -*- lexical-binding: t; -*-

;; Copyright (C) 2020-2026 emacs-lsp maintainers

;; Author: Rodrigo Kassick
;; Keywords: languages
;; Package-Requires: ((emacs "28.1") (dash "2.18.0") (spinner "1.7.3"))

;; URL: https://github.com/emacs-lsp/lsp-mode
;; 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 <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Inline Completions support
;; Specification here https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_inlineCompletion

;;; Code:

(require 'lsp-mode)

(defun lsp-inline-completion--params (implicit &optional identifier position)
  "Returns a InlineCompletionParams instance"
  (lsp-make-inline-completion-params
   :textDocument (or identifier (lsp--text-document-identifier))
   :position (or position (lsp--cur-position))
   :context (lsp-make-inline-completion-context
             :triggerKind (if implicit
                              lsp/inline-completion-trigger-automatic
                            lsp/inline-completion-trigger-invoked))))

(defun lsp-inline-completion--parse-items (response)
  "Parses the response from the server and returns a list of
InlineCompletionItem objects"

  (pcase response
    ;; Server responded with a completion list
    ((lsp-interface InlineCompletionList :items)
     (seq-into items 'list))

    ;; Server responded with a sequence of completion items
    ((pred (lambda (i)
             (and (sequencep i)
                  (lsp-inline-completion-item? (elt i 0)))))
     (seq-into response 'list))

    ;; A sequence means multiple server may have responded. Iterate over them and normalize
    ((pred sequencep)
     (let ((item-seq (cl-map 'list #'lsp-inline-completion--parse-items response)))
       (apply 'seq-concatenate `(list ,@item-seq))))))

;;;;;; Default UI -- overlay

(defun lsp-inline-completion--define-key (keymap key def &optional keep)
  "Defines key on map. With non-nil keep, will mark it as a command that
should not cancel the inline completion UI"
  (define-key keymap key def)
  (when keep
    (pcase def
      ;; Command is OK
      ((pred symbolp)
       (put def 'lsp-inline-completion-keep t))
      ;; ("str" . command) is OK
      (`(,_str . ,defn)
       (if (symbolp defn)
           (put defn 'lsp-inline-completion-keep t)
         (error "Unexpected type (str . defn) key definition %S" (type-of defn))))
      ;; Keymaps and macros are unsupported
      (t (error "Unsupported definition with keep -- %S" def)))))

(defvar lsp-inline-completion-active-map
  (let ((map (make-sparse-keymap)))
    ;; accept
    (lsp-inline-completion--define-key map (kbd "C-<return>") #'lsp-inline-completion-accept 'keep)
    (lsp-inline-completion--define-key map [mouse-1] #'lsp-inline-completion-accept-on-click  'keep)
    ;; navigate
    (lsp-inline-completion--define-key map (kbd "C-n") #'lsp-inline-completion-next 'keep)
    (lsp-inline-completion--define-key map (kbd "C-p") #'lsp-inline-completion-prev 'keep)
    ;; cancel
    (lsp-inline-completion--define-key map (kbd "C-g") #'lsp-inline-completion-cancel)
    (lsp-inline-completion--define-key map (kbd "<escape>") #'lsp-inline-completion-cancel)
    (lsp-inline-completion--define-key map (kbd "C-c C-k") #'lsp-inline-completion-cancel)
    ;; useful -- recenter without losing the completion
    (lsp-inline-completion--define-key map (kbd "C-l") #'recenter-top-bottom 'keep)
    ;; ignore
    (lsp-inline-completion--define-key map [down-mouse-1] #'ignore 'keep)
    (lsp-inline-completion--define-key map [up-mouse-1] #'ignore 'keep)
    (lsp-inline-completion--define-key map [mouse-movement] #'ignore 'keep)
    map)
  "Keymap active when showing inline code suggestions.

When adding new bindings to this map, prefer using
lsp-inline-completion--define-key. Use a non-nil keep unless the command
should cancel the completion UI")

(defsubst lsp-inline-completion--keep (cmd)
  "Returns t if the command should not cancel the current completion"
  (and (symbolp cmd) (get cmd 'lsp-inline-completion-keep)))

(defface lsp-inline-completion-overlay-face
  '((t :inherit shadow))
  "Face for the inline code suggestions overlay."
  :group 'lsp-mode)

;; Local Buffer State

(defvar-local lsp-inline-completion--items nil "The completions provided by the server.")
(defvar-local lsp-inline-completion--current nil "The current suggestion to be displayed.")
(defvar-local lsp-inline-completion--overlay nil "The overlay displaying code suggestions.")
(defvar-local lsp-inline-completion--start-point nil "The point where the completion started.")

(defcustom lsp-before-inline-completion-hook nil
  "Hooks run before starting code suggestions."
  :type 'hook
  :group 'lsp-mode)

(defcustom lsp-after-inline-completion-hook nil
  "Hooks executed after asking for code suggestions."
  :type 'hook
  :group 'lsp-mode)

(defcustom lsp-inline-completion-accepted-functions nil
  "Functions executed after accepting a code suggestion.
The functions receive the inserted text and the range that was updated by the completion."
  :type 'hook
  :group 'lsp-mode)


(defcustom lsp-inline-completion-cancelled-hook nil
  "Hooks executed after cancelling the completion UI."
  :type 'hook
  :group 'lsp-mode)

(defcustom lsp-inline-completion-before-show-hook nil
  "Hooks executed before showing a suggestion."
  :type 'hook
  :group 'lsp-mode)

(defcustom lsp-inline-completion-shown-hook nil
  "Hooks executed after showing a suggestion."
  :type 'hook
  :group 'lsp-mode)

(defcustom lsp-inline-completion-overlay-priority 9000
  "The priority of the overlay."
  :type '(choice (const :tag "No Priority" nil)
                 (integer :tag "Simple, Overriding Priority")
                 (cons :tag "Composite"
                       (choice (integer :tag "Primary")
                               (const :tag "Primary Unset" nil))
                       (integer :tag "Secondary")))
  :group 'lsp-mode)

(defsubst lsp-inline-completion--active-p ()
  "Returns whether we are in an active completion"
  (overlayp lsp-inline-completion--overlay))

(defun lsp-inline-completion--clear-overlay ()
  "Hide the suggestion overlay."
  (when (overlayp lsp-inline-completion--overlay)
    (delete-overlay lsp-inline-completion--overlay))
  (setq lsp-inline-completion--overlay nil)
  (internal-pop-keymap lsp-inline-completion-active-map 'overriding-terminal-local-map)
  (remove-hook 'pre-command-hook #'lsp-inline-completion--pre-command-hook t))


(defun lsp-inline-completion--show-keys ()
  "Shows active keymap hints in the minibuffer."

  (unless (and lsp-inline-completion--items
               (numberp lsp-inline-completion--current))
    (error "No completions to show"))

  (let ((message-log-max nil))
    (message (concat "Completion "
                     (propertize (format "%d" (1+ lsp-inline-completion--current)) 'face 'bold)
                     "/"
                     (propertize (format "%d" (length lsp-inline-completion--items)) 'face 'bold)

                     (-when-let (keys (where-is-internal #'lsp-inline-completion-next lsp-inline-completion-active-map))
                       (concat ". "
                               (propertize " Next" 'face 'italic)
                               (format ": [%s]"
                                       (string-join (--map (propertize (key-description it) 'face 'help-key-binding)
                                                           keys)
                                                    "/"))))
                     (-when-let (keys (where-is-internal #'lsp-inline-completion-accept lsp-inline-completion-active-map))
                       (concat (propertize " Accept" 'face 'italic)
                               (format ": [%s]"
                                       (string-join (--map (propertize (key-description it) 'face 'help-key-binding)
                                                           keys)
                                                    "/"))))))))


(defun lsp-inline-completion--get-overlay (beg end)
  "Build the suggestions overlay."
  (lsp-inline-completion--clear-overlay)

  (add-hook 'pre-command-hook #'lsp-inline-completion--pre-command-hook nil t)
  (setq lsp-inline-completion--overlay (make-overlay beg end nil nil t))
  (overlay-put lsp-inline-completion--overlay 'priority lsp-inline-completion-overlay-priority)
  (internal-push-keymap lsp-inline-completion-active-map 'overriding-terminal-local-map)
  (lsp-inline-completion--show-keys)

  lsp-inline-completion--overlay)


(defun lsp-inline-completion-show-overlay ()
  "Makes the suggestion overlay visible."
  (unless (and lsp-inline-completion--items
               (numberp lsp-inline-completion--current))
    (error "No completions to show"))

  (lsp-inline-completion--clear-overlay)

  (run-hooks 'lsp-inline-completion-before-show-hook)

  (-let* ((suggestion
           (elt lsp-inline-completion--items
                lsp-inline-completion--current))
          ((&InlineCompletionItem? :insert-text :range? (&RangeToPoint :start :end)) suggestion)
          (start-point (or start (point)))
          (showing-at-eol (save-excursion
                            (goto-char start-point)
                            (and (not (bolp)) (eolp))))
          (beg (if showing-at-eol (1- start-point) start-point))
          (end-point  (or end (1+ beg)))
          (text (cond
                 ((lsp-markup-content? insert-text) (lsp:markup-content-value insert-text))
                 (t insert-text)))
          (propertized-text (concat
                             (buffer-substring beg start-point)
                             (propertize text 'face 'lsp-inline-completion-overlay-face)))
          (ov (lsp-inline-completion--get-overlay beg end-point))
          display-str after-str target-position)

    (goto-char beg)

    (put-text-property 0 (length propertized-text) 'cursor t propertized-text)

    (if (string-prefix-p
         (buffer-substring-no-properties beg lsp-inline-completion--start-point)
         text)
        (progn
          ;; Show the prefix as `display'
          (setq display-str (substring propertized-text 0 (- lsp-inline-completion--start-point beg)))
          (setq after-str (substring propertized-text (- lsp-inline-completion--start-point beg) nil))
          (setq target-position lsp-inline-completion--start-point))
      (setq display-str (substring propertized-text 0 1))
      (setq after-str (substring propertized-text 1))
      (setq target-position beg))

    (overlay-put ov 'display display-str)
    (overlay-put ov 'after-string after-str)

    (goto-char target-position)

    (run-hooks 'lsp-inline-completion-shown-hook)))

(defun lsp-inline-completion--insert-sugestion (text kind start end command?)
  (let* ((text-insert-start (or start lsp-inline-completion--start-point))
         text-insert-end)
    (when text-insert-start
      (goto-char text-insert-start))

    ;; When range is provided, must replace the text of the range by the text
    ;; to insert
    (when (and start end (/= start end))
      (delete-region start end))

    ;; Insert suggestion, keeping the cursor at the start point
    (insert text)

    (setq text-insert-end (point))

    ;; If a template, format it -- keep track of the end position!
    (when (eq kind 'snippet)
      (let ((end-marker (set-marker (make-marker) (point))))
        (lsp--expand-snippet (buffer-substring text-insert-start text-insert-end)
                             text-insert-start
                             text-insert-end)
        (setq text-insert-end (marker-position end-marker))
        (set-marker end-marker nil)))

    ;; Post command
    (when command?
      (lsp--execute-command command?))

    ;; hooks
    (run-hook-with-args 'lsp-inline-completion-accepted-functions text text-insert-start text-insert-end)))

(defun lsp-inline-completion-accept ()
  "Accepts the current suggestion."
  (interactive)
  (unless (lsp-inline-completion--active-p)
     (error "Not showing suggestions"))

  (lsp-inline-completion--clear-overlay)

  (-let* ((suggestion (elt lsp-inline-completion--items lsp-inline-completion--current))
          ((&InlineCompletionItem? :insert-text :range? :command?) suggestion)
          ((kind . text) (cond
                          ((lsp-markup-content? insert-text)
                           (cons 'snippet (lsp:markup-content-value insert-text) ))
                          (t (cons 'text insert-text))))
          ((start . end) (when range?
                           (-let (((&RangeToPoint :start :end) range?)) (cons start end)))))

    (with-no-warnings
      ;; Compiler does not believes this macro is defined
      (lsp-with-undo-amalgamate
        (lsp-inline-completion--insert-sugestion text kind start end command?)))))

(defun lsp-inline-completion-accept-on-click (event)
  (interactive "e")

  (lsp-inline-completion-accept)
  (-let (((col . row) (posn-actual-col-row (event-end event))))
    (move-to-window-line row)
    (beginning-of-line)
    (forward-char (- col
                     (if (bound-and-true-p display-line-numbers-mode)
                         (+ 2 (line-number-display-width))
                       0)))))

(defun lsp-inline-completion-cancel ()
  "Close the suggestion overlay."
  (interactive)
  (message nil) ;; clear echo
  (let ((was-active (lsp-inline-completion--active-p)))
    (lsp-inline-completion--clear-overlay)

    (when was-active
      (goto-char lsp-inline-completion--start-point)
      (run-hooks 'lsp-inline-completion-cancelled-hook))))

(defun lsp-inline-completion-next ()
  "Display the next inline completion."
  (interactive)
  (unless (lsp-inline-completion--active-p)
    (error "Not showing suggestions"))

  (setq lsp-inline-completion--current
        (mod (1+ lsp-inline-completion--current)
             (length lsp-inline-completion--items)))

  (lsp-inline-completion-show-overlay))

(defun lsp-inline-completion-prev ()
  "Display the previous inline completion."
  (interactive)

  (unless (lsp-inline-completion--active-p)
    (error "Not showing suggestions"))

  (setq lsp-inline-completion--current
        (mod (1- lsp-inline-completion--current)
             (length lsp-inline-completion--items)))

  (lsp-inline-completion-show-overlay))

;;;###autoload
(defun lsp-inline-completion-display (&optional implicit)
  "Displays the inline completions overlay."
  (interactive)

  (unless implicit
    (lsp--spinner-start))

  (run-hooks 'lsp-before-inline-completion-hook)

  (condition-case err
      (unwind-protect
          (if-let* ((resp (lsp-request-while-no-input "textDocument/inlineCompletion"
                                                      (lsp-inline-completion--params implicit)))
                    (items (lsp-inline-completion--parse-items resp)))

              (progn
                (lsp-inline-completion--clear-overlay)
                (setq lsp-inline-completion--items items)
                (setq lsp-inline-completion--current 0)
                (setq lsp-inline-completion--start-point (point))
                (lsp-inline-completion-show-overlay))
            (unless implicit
              (lsp--info "No Suggestions!")))
        ;; Clean up
        (unless implicit
          (lsp--spinner-stop)))
    (t (lsp--error "Could not fetch completions: %s" err))))

(defun lsp-inline-completion--pre-command-hook ()
  "Cancels the inline completio before a non-keep command"
  (when (and (lsp-inline-completion--active-p)
             (not (lsp-inline-completion--keep this-command)))
    (lsp-inline-completion-cancel)))


;; Inline Completion Mode
;;;###autoload
(defcustom lsp-inline-completion-enable t
  "If non-nil it will enable inline completions on idle."
  :type 'boolean
  :group 'lsp-mode
  :package-version '(lsp-mode . "10.0.0"))

(defcustom lsp-inline-completion-idle-delay 2
  "The number of seconds before trying to fetch inline completions, when
lsp-inline-completion-mode is active."
  :type 'number
  :group 'lsp-mode
  :package-version '(lsp-mode . "10.0.0"))

(defcustom lsp-inline-completion-inhibit-predicates nil
  "When a function of this list returns non nil, lsp-inline-completion-mode will not show the completion."
  :type '(repeat function)
  :group 'lsp-mode)

(defvar-local lsp-inline-completion--idle-timer nil
  "The idle timer used by lsp-inline-completion-mode.")

;;;###autoload
(define-minor-mode lsp-inline-completion-mode
  "Mode automatically displaying inline completions."
  :lighter nil
  (cond
   ((and lsp-inline-completion-mode lsp--buffer-workspaces)
    (add-hook 'lsp-on-change-hook #'lsp-inline-completion--after-change nil t))

   (t
    (when lsp-inline-completion--idle-timer
      (cancel-timer lsp-inline-completion--idle-timer))

    (lsp-inline-completion-cancel)

    (remove-hook 'lsp-on-change-hook #'lsp-inline-completion--after-change t))))

(defun lsp-inline-completion--maybe-display (original-buffer original-point)
  ;; This is executed on an idle timer -- ensure state did not change before
  ;; displaying
  (when (and (buffer-live-p original-buffer)
             (eq (current-buffer) original-buffer)
             (eq (point) original-point)
             (--none? (funcall it) lsp-inline-completion-inhibit-predicates))
    (setq last-command this-command)
    (setq this-command 'lsp-inline-completion-display)
    (lsp-inline-completion-display 'implicit)))

(defun lsp-inline-completion--after-change (&rest _)
  ;; This function is in lsp-on-change-hooks, which is executed on a timer by
  ;; lsp-on-change. Do not assume that the buffer/window state has not been
  ;; modified in the meantime! Use the values in lsp--after-change-vals to
  ;; ensure this.

  (when lsp-inline-completion--idle-timer
    (cancel-timer lsp-inline-completion--idle-timer))

  (when (and lsp-inline-completion-mode lsp--buffer-workspaces)
    (let ((original-buffer (plist-get lsp--after-change-vals :buffer))
          (original-point (plist-get lsp--after-change-vals :point)))
      (setq lsp-inline-completion--idle-timer
            (run-with-idle-timer lsp-inline-completion-idle-delay
                                 nil
                                 #'lsp-inline-completion--maybe-display
                                 original-buffer
                                 original-point)))))

;;;###autoload
(add-hook 'lsp-configure-hook (lambda ()
                                (when (and lsp-inline-completion-enable
                                           (lsp-feature? "textDocument/inlineCompletion"))
                                  (lsp-inline-completion-mode))))

;; Company default integration

(declare-function company--active-p "ext:company")
(declare-function company-cancel "ext:company" (&optional result))
(declare-function company-manual-begin "ext:company")
(defvar company--begin-inhibit-commands)
(defcustom lsp-inline-completion-mode-inhibit-when-company-active t
  "If the inline completion mode should avoid calling completions when company is active."
  :type 'boolean
  :group 'lsp-mode)

(defvar-local lsp-inline-completion--showing-company nil "If company was active when the tooltip is shown.")

(defun lsp-inline-completion--company-save-state-and-hide ()
  (setq lsp-inline-completion--showing-company
        (and (bound-and-true-p company-mode)
             (company--active-p)))

  (when lsp-inline-completion--showing-company
    (company-cancel)))

(defun lsp-inline-completion--company-restore-state ()
  (when lsp-inline-completion--showing-company
      (company-manual-begin))
  (setq lsp-inline-completion--showing-company nil))

(defun lsp-inline-completion--company-active-p ()
  (and (bound-and-true-p company-mode) (company--active-p)))

;;;###autoload
(define-minor-mode lsp-inline-completion-company-integration-mode
  "Minor mode to be used when company mode is active with lsp-inline-completion-mode."
  :lighter nil
  (cond
   ((and lsp-inline-completion-company-integration-mode lsp--buffer-workspaces (bound-and-true-p company-mode))
    (add-hook 'lsp-inline-completion-before-show-hook #'lsp-inline-completion--company-save-state-and-hide nil t)
    (add-hook 'lsp-inline-completion-cancelled-hook #'lsp-inline-completion--company-restore-state nil t)
    (unless (memq #'lsp-inline-completion-display company--begin-inhibit-commands)
      (setq-local company--begin-inhibit-commands
                  (cons #'lsp-inline-completion-display company--begin-inhibit-commands)))
    (when (and lsp-inline-completion-mode-inhibit-when-company-active
               (not (memq  #'lsp-inline-completion--company-active-p lsp-inline-completion-inhibit-predicates)))
      (setq-local lsp-inline-completion-inhibit-predicates
                  (cons #'lsp-inline-completion--company-active-p lsp-inline-completion-inhibit-predicates))))

   (t
    (remove-hook 'lsp-inline-completion-before-show-hook #'lsp-inline-completion--company-save-state-and-hide t)
    (remove-hook 'lsp-inline-completion-cancelled-hook #'lsp-inline-completion--company-save-state-and-hide t)
    (when (boundp 'company--begin-inhibit-commands)
      (setq-local company--begin-inhibit-commands (delq #'lsp-inline-completion-display company--begin-inhibit-commands)))
    (setq-local lsp-inline-completion-inhibit-predicates
          (delq #'lsp-inline-completion--company-active-p lsp-inline-completion-inhibit-predicates)))))

(provide 'lsp-inline-completion)
;;; lsp-inline-completion.el ends here
