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