;;; 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 .
;;; 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-") #'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 "") #'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