config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

lsp-inline-completion.el (21216B)


      1 ;;; lsp-inline-completion.el --- LSP mode                              -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2020-2024 emacs-lsp maintainers
      4 
      5 ;; Author: Rodrigo Kassick
      6 ;; Keywords: languages
      7 ;; Package-Requires: ((emacs "27.1") (dash "2.18.0") (spinner "1.7.3"))
      8 
      9 ;; URL: https://github.com/emacs-lsp/lsp-mode
     10 ;; This program is free software; you can redistribute it and/or modify
     11 ;; it under the terms of the GNU General Public License as published by
     12 ;; the Free Software Foundation, either version 3 of the License, or
     13 ;; (at your option) any later version.
     14 
     15 ;; This program is distributed in the hope that it will be useful,
     16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18 ;; GNU General Public License for more details.
     19 
     20 ;; You should have received a copy of the GNU General Public License
     21 ;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
     22 
     23 ;;; Commentary:
     24 
     25 ;; Inline Completions support
     26 ;; Specification here https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_inlineCompletion
     27 
     28 ;;; Code:
     29 
     30 (require 'lsp-mode)
     31 
     32 (defun lsp-inline-completion--params (implicit &optional identifier position)
     33   "Returns a InlineCompletionParams instance"
     34   (lsp-make-inline-completion-params
     35    :textDocument (or identifier (lsp--text-document-identifier))
     36    :position (or position (lsp--cur-position))
     37    :context (lsp-make-inline-completion-context
     38              :triggerKind (if implicit
     39                               lsp/inline-completion-trigger-automatic
     40                             lsp/inline-completion-trigger-invoked))))
     41 
     42 (defun lsp-inline-completion--parse-items (response)
     43   "Parses the reponse from the server and returns a list of
     44 InlineCompletionItem objects"
     45 
     46   (pcase response
     47     ;; Server responded with a completion list
     48     ((lsp-interface InlineCompletionList :items)
     49      (seq-into items 'list))
     50 
     51     ;; Server responded with a sequence of completion items
     52     ((pred (lambda (i)
     53              (and (sequencep i)
     54                   (lsp-inline-completion-item? (elt i 0)))))
     55      (seq-into response 'list))
     56 
     57     ;; A sequence means multiple server may have responded. Iterate over them and normalize
     58     ((pred sequencep)
     59      (let ((item-seq (cl-map 'list #'lsp-inline-completion--parse-items response)))
     60        (apply 'seq-concatenate `(list ,@item-seq))))))
     61 
     62 ;;;;;; Default UI -- overlay
     63 
     64 (defvar lsp-inline-completion-active-map
     65   (let ((map (make-sparse-keymap)))
     66     ;; accept
     67     (define-key map (kbd "C-<return>") #'lsp-inline-completion-accept)
     68     (define-key map [mouse-1] #'lsp-inline-completion-accept-on-click)
     69     ;; navigate
     70     (define-key map (kbd "C-n") #'lsp-inline-completion-next)
     71     (define-key map (kbd "C-p") #'lsp-inline-completion-prev)
     72     ;; cancel
     73     (define-key map (kbd "C-g") #'lsp-inline-completion-cancel)
     74     (define-key map (kbd "<escape>") #'lsp-inline-completion-cancel)
     75     (define-key map (kbd "C-c C-k") #'lsp-inline-completion-cancel)
     76     ;; useful -- recenter without loosing the completion
     77     (define-key map (kbd "C-l") #'recenter-top-bottom)
     78     ;; ignore
     79      (define-key map [down-mouse-1] #'ignore)
     80     (define-key map [up-mouse-1] #'ignore)
     81     (define-key map [mouse-movement] #'ignore)
     82     ;; Any event outside of the map, cancel and use it
     83     (define-key map [t] #'lsp-inline-completion-cancel-with-input)
     84     map)
     85   "Keymap active when showing inline code suggestions")
     86 
     87 (defface lsp-inline-completion-overlay-face
     88   '((t :inherit shadow))
     89   "Face for the inline code suggestions overlay."
     90   :group 'lsp-mode)
     91 
     92 ;; Local Buffer State
     93 
     94 (defvar-local lsp-inline-completion--items nil "The completions provided by the server")
     95 (defvar-local lsp-inline-completion--current nil "The current suggestion to be displayed")
     96 (defvar-local lsp-inline-completion--overlay nil "The overlay displaying code suggestions")
     97 (defvar-local lsp-inline-completion--start-point nil "The point where the completion started")
     98 
     99 (defcustom lsp-before-inline-completion-hook nil
    100   "Hooks run before starting code suggestions"
    101   :type 'hook
    102   :group 'lsp-mode)
    103 
    104 (defcustom lsp-after-inline-completion-hook nil
    105   "Hooks executed after asking for code suggestions."
    106   :type 'hook
    107   :group 'lsp-mode)
    108 
    109 (defcustom lsp-inline-completion-accepted-hook nil
    110   "Hooks executed after accepting a code suggestion. The hooks receive the
    111 text range that was updated by the completion"
    112   :type 'hook
    113   :group 'lsp-mode)
    114 
    115 (defcustom lsp-inline-completion-cancelled-hook nil
    116   "Hooks executed after cancelling the completion UI"
    117   :type 'hook
    118   :group 'lsp-mode)
    119 
    120 (defcustom lsp-inline-completion-before-show-hook nil
    121   "Hooks executed before showing a suggestion."
    122   :type 'hook
    123   :group 'lsp-mode)
    124 
    125 (defcustom lsp-inline-completion-shown-hook nil
    126   "Hooks executed after showing a suggestion."
    127   :type 'hook
    128   :group 'lsp-mode)
    129 
    130 (defcustom lsp-inline-completion-overlay-priority 9000
    131   "The priority of the overlay."
    132   :type '(choice (const :tag "No Priority" nil)
    133                  (integer :tag "Simple, Overriding Priority")
    134                  (cons :tag "Composite"
    135                        (choice (integer :tag "Primary")
    136                                (const :tag "Primary Unset" nil))
    137                        (integer :tag "Secondary")))
    138   :group 'lsp-mode)
    139 
    140 (defsubst lsp-inline-completion--overlay-visible ()
    141   "Return whether the `overlay' is avaiable."
    142   (and (overlayp lsp-inline-completion--overlay)
    143        (overlay-buffer lsp-inline-completion--overlay)))
    144 
    145 (defun lsp-inline-completion--clear-overlay ()
    146   "Hide the suggestion overlay"
    147   (when (lsp-inline-completion--overlay-visible)
    148     (delete-overlay lsp-inline-completion--overlay))
    149   (setq lsp-inline-completion--overlay nil))
    150 
    151 
    152 (defun lsp-inline-completion--get-overlay (beg end)
    153   "Build the suggestions overlay"
    154   (when (overlayp lsp-inline-completion--overlay)
    155     (lsp-inline-completion--clear-overlay))
    156 
    157   (setq lsp-inline-completion--overlay (make-overlay beg end nil nil t))
    158   (overlay-put lsp-inline-completion--overlay 'keymap lsp-inline-completion-active-map)
    159   (overlay-put lsp-inline-completion--overlay 'priority lsp-inline-completion-overlay-priority)
    160 
    161   lsp-inline-completion--overlay)
    162 
    163 
    164 (defun lsp-inline-completion--show-keys ()
    165   "Shows active keymap hints in the minibuffer"
    166 
    167   (unless (and lsp-inline-completion--items
    168                (numberp lsp-inline-completion--current))
    169     (error "No completions to show"))
    170 
    171   (let ((message-log-max nil))
    172     (message (concat "Completion "
    173                      (propertize (format "%d" (1+ lsp-inline-completion--current)) 'face 'bold)
    174                      "/"
    175                      (propertize (format "%d" (length lsp-inline-completion--items)) 'face 'bold)
    176 
    177                      (-when-let (keys (where-is-internal #'lsp-inline-completion-next lsp-inline-completion-active-map))
    178                        (concat ". "
    179                                (propertize " Next" 'face 'italic)
    180                                (format ": [%s]"
    181                                        (string-join (--map (propertize (key-description it) 'face 'help-key-binding)
    182                                                            keys)
    183                                                     "/"))))
    184                      (-when-let (keys (where-is-internal #'lsp-inline-completion-accept lsp-inline-completion-active-map))
    185                        (concat (propertize " Accept" 'face 'italic)
    186                                (format ": [%s]"
    187                                        (string-join (--map (propertize (key-description it) 'face 'help-key-binding)
    188                                                            keys)
    189                                                     "/"))))))))
    190 
    191 (defun lsp-inline-completion-show-overlay ()
    192   "Makes the suggestion overlay visible"
    193   (unless (and lsp-inline-completion--items
    194                (numberp lsp-inline-completion--current))
    195     (error "No completions to show"))
    196 
    197   (lsp-inline-completion--clear-overlay)
    198 
    199   (run-hooks 'lsp-inline-completion-before-show-hook)
    200 
    201   (-let* ((suggestion
    202            (elt lsp-inline-completion--items
    203                 lsp-inline-completion--current))
    204           ((&InlineCompletionItem? :insert-text :range?) suggestion)
    205           ((&RangeToPoint :start :end) range?)
    206           (start-point (or start (point)))
    207           (showing-at-eol (save-excursion
    208                             (goto-char start-point)
    209                             (eolp)))
    210           (beg (if showing-at-eol (1- start-point) start-point))
    211           (end-point  (or end (1+ beg)))
    212           (text (cond
    213                  ((lsp-markup-content? insert-text) (lsp:markup-content-value insert-text))
    214                  (t insert-text)))
    215           (propertizedText (concat
    216                             (buffer-substring beg start-point)
    217                             (propertize text 'face 'lsp-inline-completion-overlay-face)))
    218           (ov (lsp-inline-completion--get-overlay beg end-point))
    219           (completion-is-substr (string-equal
    220                                  (buffer-substring beg lsp-inline-completion--start-point)
    221                                  (substring propertizedText 0 (- lsp-inline-completion--start-point beg))))
    222           display-str after-str target-position)
    223 
    224     (goto-char beg)
    225 
    226     (put-text-property 0 (length propertizedText) 'cursor t propertizedText)
    227 
    228     (if completion-is-substr
    229         (progn
    230           ;; Show the prefix as `display'
    231           (setq display-str (substring propertizedText 0 (- lsp-inline-completion--start-point beg)))
    232           (setq after-str (substring propertizedText (- lsp-inline-completion--start-point beg) nil))
    233           (setq target-position lsp-inline-completion--start-point))
    234 
    235 
    236       (setq display-str (substring propertizedText 0 1))
    237       (setq after-str (substring propertizedText 1))
    238       (setq target-position beg))
    239 
    240     (overlay-put ov 'display display-str)
    241     (overlay-put ov 'after-string after-str)
    242 
    243     (goto-char target-position)
    244 
    245     (lsp-inline-completion--show-keys)
    246     (run-hooks 'lsp-inline-completion-shown-hook)))
    247 
    248 (defun lsp-inline-completion--insert-sugestion (text kind start end command?)
    249   (let* ((text-insert-start (or start lsp-inline-completion--start-point))
    250          text-insert-end
    251          (completion-is-substr (string-equal
    252                                 (buffer-substring text-insert-start lsp-inline-completion--start-point)
    253                                 (substring text 0 (- lsp-inline-completion--start-point text-insert-start)))))
    254     (when text-insert-start
    255       (goto-char text-insert-start))
    256 
    257     ;; When range is provided, must replace the text of the range by the text
    258     ;; to insert
    259     (when (and start end (/= start end))
    260       (delete-region start end))
    261 
    262     ;; Insert suggestion, keeping the cursor at the start point
    263     (insert text)
    264 
    265     (setq text-insert-end (point))
    266 
    267     ;; If a template, format it -- keep track of the end position!
    268     (when (eq kind 'snippet)
    269       (let ((end-marker (set-marker (make-marker) (point))))
    270         (lsp--expand-snippet (buffer-substring text-insert-start text-insert-end)
    271                              text-insert-start
    272                              text-insert-end)
    273         (setq text-insert-end (marker-position end-marker))
    274         (set-marker end-marker nil)))
    275 
    276     ;; Post command
    277     (when command?
    278       (lsp--execute-command command?))
    279 
    280     (if completion-is-substr
    281         (goto-char lsp-inline-completion--start-point)
    282       (goto-char text-insert-start))
    283 
    284     ;; hooks
    285     (run-hook-with-args-until-failure 'lsp-inline-completion-accepted-hook text text-insert-start text-insert-end)))
    286 
    287 (defun lsp-inline-completion-accept ()
    288   "Accepts the current suggestion"
    289   (interactive)
    290   (unless (lsp-inline-completion--overlay-visible)
    291     (error "Not showing suggestions"))
    292 
    293   (lsp-inline-completion--clear-overlay)
    294   (-let* ((suggestion (elt lsp-inline-completion--items lsp-inline-completion--current))
    295           ((&InlineCompletionItem? :insert-text :range? :command?) suggestion)
    296           ((kind . text) (cond
    297                           ((lsp-markup-content? insert-text)
    298                            (cons 'snippet (lsp:markup-content-value insert-text) ))
    299                           (t (cons 'text insert-text))))
    300           ((start . end) (when range?
    301                            (-let (((&RangeToPoint :start :end) range?)) (cons start end)))))
    302 
    303     (with-no-warnings
    304       ;; Compiler does not believes this macro is defined
    305       (lsp-with-undo-amalgamate
    306         (lsp-inline-completion--insert-sugestion text kind start end command?)))))
    307 
    308 (defun lsp-inline-completion-accept-on-click (event)
    309   (interactive "e")
    310 
    311   (lsp-inline-completion-accept)
    312   (-let (((col . row) (posn-actual-col-row (event-end event))))
    313     (move-to-window-line row)
    314     (beginning-of-line)
    315     (forward-char (- col
    316                      (if (bound-and-true-p display-line-numbers-mode)
    317                          (+ 2 (line-number-display-width))
    318                        0)))))
    319 
    320 (defun lsp-inline-completion-cancel ()
    321   "Close the suggestion overlay"
    322   (interactive)
    323   (when (lsp-inline-completion--overlay-visible)
    324 
    325     (lsp-inline-completion--clear-overlay)
    326 
    327     (when lsp-inline-completion--start-point
    328       (goto-char lsp-inline-completion--start-point))
    329 
    330     (run-hooks 'lsp-inline-completion-cancelled-hook)))
    331 
    332 (defun lsp-inline-completion-cancel-with-input (event &optional arg)
    333   "Cancel the inline completion and executes whatever event was received"
    334   (interactive (list last-input-event current-prefix-arg))
    335 
    336   (lsp-inline-completion-cancel)
    337 
    338   (let ((command (lookup-key (current-active-maps) (vector event)))
    339         (current-prefix-arg arg))
    340 
    341     (when (commandp command)
    342       (call-interactively command))))
    343 
    344 (defun lsp-inline-completion-next ()
    345   "Display the next inline completion"
    346   (interactive)
    347   (unless (lsp-inline-completion--overlay-visible)
    348     (error "Not showing suggestions"))
    349   (setq lsp-inline-completion--current
    350         (mod (1+ lsp-inline-completion--current)
    351              (length lsp-inline-completion--items)))
    352 
    353   (lsp-inline-completion-show-overlay))
    354 
    355 (defun lsp-inline-completion-prev ()
    356   "Display the previous inline completion"
    357   (interactive)
    358   (unless (lsp-inline-completion--overlay-visible)
    359     (error "Not showing suggestions"))
    360   (setq lsp-inline-completion--current
    361         (mod (1- lsp-inline-completion--current)
    362              (length lsp-inline-completion--items)))
    363 
    364   (lsp-inline-completion-show-overlay))
    365 
    366 ;;;###autoload
    367 (defun lsp-inline-completion-display (&optional implicit)
    368   "Displays the inline completions overlay"
    369   (interactive)
    370 
    371   (unless implicit
    372     (lsp--spinner-start) )
    373 
    374   (unwind-protect
    375       (if-let* ((resp (lsp-request-while-no-input "textDocument/inlineCompletion"
    376                                                   (lsp-inline-completion--params implicit)))
    377                 (items (lsp-inline-completion--parse-items resp)))
    378 
    379           (progn
    380             (lsp-inline-completion--clear-overlay)
    381             (setq lsp-inline-completion--items items)
    382             (setq lsp-inline-completion--current 0)
    383             (setq lsp-inline-completion--start-point (point))
    384             (lsp-inline-completion-show-overlay))
    385         (unless implicit
    386           (lsp--info "No Suggestions!")))
    387     ;; Clean up
    388     (unless implicit
    389       (lsp--spinner-stop))))
    390 
    391 
    392 ;; Inline Completion Mode
    393 ;;;###autoload
    394 (defcustom lsp-inline-completion-enable t
    395   "If non-nil it will enable inline completions on idle."
    396   :type 'boolean
    397   :group 'lsp-mode
    398   :package-version '(lsp-mode . "9.0.1"))
    399 
    400 (defcustom lsp-inline-completion-idle-delay 2
    401   "The number of seconds before trying to fetch inline completions, when
    402 lsp-inline-completion-mode is active"
    403   :type 'number
    404   :group 'lsp-mode
    405   :package-version '(lsp-mode . "9.0.1"))
    406 
    407 (defcustom lsp-inline-completion-inhibit-predicates nil
    408   "When a function of this list returns non nil, lsp-inline-completion-mode will not show the completion"
    409   :type '(repeat function)
    410   :group 'lsp-mode)
    411 
    412 (defvar-local lsp-inline-completion--idle-timer nil
    413   "The idle timer used by lsp-inline-completion-mode")
    414 
    415 ;;;###autoload
    416 (define-minor-mode lsp-inline-completion-mode
    417   "Mode automatically displaying inline completions."
    418   :lighter nil
    419   (cond
    420    ((and lsp-inline-completion-mode lsp--buffer-workspaces)
    421     (add-hook 'lsp-on-change-hook #'lsp-inline-completion--after-change nil t))
    422    (t
    423     (when lsp-inline-completion--idle-timer
    424       (cancel-timer lsp-inline-completion--idle-timer))
    425 
    426     (lsp-inline-completion-cancel)
    427 
    428     (remove-hook 'lsp-on-change-hook #'lsp-inline-completion--after-change t))))
    429 
    430 (defun lsp-inline-completion--maybe-display (original-buffer original-point)
    431   ;; This is executed on an idle timer -- ensure state did not change before
    432   ;; displaying
    433   (when (and (buffer-live-p original-buffer)
    434              (eq (current-buffer) original-buffer)
    435              (eq (point) original-point)
    436              (--none? (funcall it) lsp-inline-completion-inhibit-predicates))
    437     (setq last-command this-command)
    438     (setq this-command 'lsp-inline-completion-display)
    439     (lsp-inline-completion-display 'implicit)))
    440 
    441 (defun lsp-inline-completion--after-change (&rest _)
    442   ;; This function is in lsp-on-change-hooks, which is executed on a timer by
    443   ;; lsp-on-change. Do not assume that the buffer/window state has not been
    444   ;; modified in the meantime! Use the values in lsp--after-change-vals to
    445   ;; ensure this.
    446 
    447   (when lsp-inline-completion--idle-timer
    448     (cancel-timer lsp-inline-completion--idle-timer))
    449 
    450   (when (and lsp-inline-completion-mode lsp--buffer-workspaces)
    451     (let ((original-buffer (plist-get lsp--after-change-vals :buffer))
    452           (original-point (plist-get lsp--after-change-vals :point)))
    453       (setq lsp-inline-completion--idle-timer
    454             (run-with-idle-timer lsp-inline-completion-idle-delay
    455                                  nil
    456                                  #'lsp-inline-completion--maybe-display
    457                                  original-buffer
    458                                  original-point)))))
    459 
    460 ;;;###autoload
    461 (add-hook 'lsp-configure-hook (lambda ()
    462                                 (when (and lsp-inline-completion-enable
    463                                            (lsp-feature? "textDocument/inlineCompletion"))
    464                                   (lsp-inline-completion-mode))))
    465 
    466 ;; Company default integration
    467 
    468 (declare-function company--active-p "ext:company")
    469 (declare-function company-cancel "ext:company" (&optional result))
    470 (declare-function company-manual-begin "ext:company")
    471 (defvar company--begin-inhibit-commands)
    472 (defcustom lsp-inline-completion-mode-inhibit-when-company-active t
    473   "If the inline completion mode should avoid calling completions when company is active"
    474   :type 'boolean
    475   :group 'lsp-mode)
    476 
    477 (defvar-local lsp-inline-completion--showing-company nil "If company was active when the tooltip is shown")
    478 
    479 (defun lsp-inline-completion--company-save-state-and-hide ()
    480   (setq lsp-inline-completion--showing-company
    481         (and (bound-and-true-p company-mode)
    482              (company--active-p)))
    483 
    484   (when lsp-inline-completion--showing-company
    485     (company-cancel)))
    486 
    487 (defun lsp-inline-completion--company-restore-state ()
    488   (when lsp-inline-completion--showing-company
    489       (company-manual-begin))
    490   (setq lsp-inline-completion--showing-company nil))
    491 
    492 (defun lsp-inline-completion--company-active-p ()
    493   (and (bound-and-true-p company-mode) (company--active-p)))
    494 
    495 ;;;###autoload
    496 (define-minor-mode lsp-inline-completion-company-integration-mode
    497   "Minor mode to be used when company mode is active with lsp-inline-completion-mode"
    498   :lighter nil
    499   (cond
    500    ((and lsp-inline-completion-company-integration-mode lsp--buffer-workspaces (bound-and-true-p company-mode))
    501     (add-hook 'lsp-inline-completion-before-show-hook #'lsp-inline-completion--company-save-state-and-hide nil t)
    502     (add-hook 'lsp-inline-completion-cancelled-hook #'lsp-inline-completion--company-restore-state nil t)
    503     (unless (memq #'lsp-inline-completion-display company--begin-inhibit-commands)
    504       (setq-local company--begin-inhibit-commands
    505                   (cons #'lsp-inline-completion-display company--begin-inhibit-commands)))
    506     (when (and lsp-inline-completion-mode-inhibit-when-company-active
    507                (not (memq  #'lsp-inline-completion--company-active-p lsp-inline-completion-inhibit-predicates)))
    508       (setq-local lsp-inline-completion-inhibit-predicates
    509                   (cons #'lsp-inline-completion--company-active-p lsp-inline-completion-inhibit-predicates))))
    510 
    511    (t
    512     (remove-hook 'lsp-inline-completion-before-show-hook #'lsp-inline-completion--company-save-state-and-hide t)
    513     (remove-hook 'lsp-inline-completion-cancelled-hook #'lsp-inline-completion--company-save-state-and-hide t)
    514     (when (boundp 'company--begin-inhibit-commands)
    515       (setq-local company--begin-inhibit-commands (delq #'lsp-inline-completion-display company--begin-inhibit-commands)))
    516     (setq-local lsp-inline-completion-inhibit-predicates
    517           (delq #'lsp-inline-completion--company-active-p lsp-inline-completion-inhibit-predicates)))))
    518 
    519 (provide 'lsp-inline-completion)