Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: inline completions #4623

Open
wants to merge 34 commits into
base: master
Choose a base branch
from

Conversation

kassick
Copy link
Contributor

@kassick kassick commented Nov 25, 2024

As discussed in #4581, this PR adds support for Inline Completions

Using some server with inlineCompletionProvider capability (I'm using the copilot-node-server with this implementation), the inline completion is triggered automatically if lsp-inline-completion-enable is set to t. The user can also call lsp-inline-completion-display explicitly to have the completion displayed.

Captura de tela de 2024-11-25 10-50-01

Kooha-2024-11-25-10-51-45

Optionally, the user can add lsp-inline-completion-show-keys to the lsp-inline-completion-shown-hook and the number of available completions and the keys used to interact with the tooltips are shown in the minibuffer.

image

When the inline completion tooltip is being displayed, pressing any key that is not mapped in lsp-inline-completion-active-map causes the tooltip to be hidden and the key is processed with the current active keymap. This way, if the completion is shown automatically, the user can continue typing to have the completion ignored.

@kassick kassick force-pushed the feat/inline-completions branch 5 times, most recently from bf4ab0a to 71c81dd Compare November 25, 2024 14:45
@kassick
Copy link
Contributor Author

kassick commented Nov 26, 2024

A note about company mode and the inline completion overlay: Since the completions are not provided by company, we may end up with the company popup displayed alongside (or over) the inline completion overlay (as it happened already with copilot.el).

To workaround this issue, I've added code to cancel company before showing the overlay and then restore it on cancel.

Kooha-2024-11-26-14-40-17.webm

Switching between the two completion UIs can be achieved by binding company-manual-begin and lsp-inline-completion-display.

Nonetheless, I've added a variable that can inhibit inline completion mode on certain conditions, such as company being active. For example, I have the following snippet in my user settings:

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

  (push 'lsp-inline-completion-inhibit-if-company-active lsp-inline-completion-inhibit-predicates)

Add a list of predicates that can inhibit triggering inline completions --
e.g.:

    ```elisp
    (defun lsp-inline-completion-inhibit-if-company-active ()
        (and (bound-and-true-p company-mode) (company--active-p)))

      (push 'lsp-inline-completion-inhibit-if-company-active lsp-inline-completion-inhibit-predicates)
    ```
lsp-protocol.el Outdated
@@ -624,6 +626,8 @@ See `-let' for a description of the destructuring mechanism."
(CompletionItemKindCapabilities nil (:valueSet))
(CompletionItemTagSupportCapabilities (:valueSet) nil)
(CompletionOptions nil (:resolveProvider :triggerCharacters :allCommitCharacters))
(InlineCompletionItem (:insertText) (:filterText :range :command))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sort this

Copy link
Contributor Author

@kassick kassick Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the relevant interfaces to the end of the list with the version of the protocol that supports them.

Let me know if you want to have the whole interface definition sorted.

lsp/inline-completion-trigger-automatic
lsp/inline-completion-trigger-invoked)))))

;;;###autoload
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to make this function public and have autoload. Shouldn't it be used for internal function only?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was trying to make the test suite happy when I have the minor mode defined in lsp-mode.el; forgot to remove it later

;; Kludge to workaround multiple backends responding -- it may
;; come as a list (multiple servers) or as a single ht (single
;; server). Promote it to list and move on
(if (ht-p it) (list it) it)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type can be ht or plist depend on lsp-use-plists so it would be better to just check the lsp-inline-completion-list? and then merge or extract and merge items.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call!
Though we must check if it is a completion-list or a completion-list-items

result: InlineCompletionItem[] | InlineCompletionList | null defined as follows:

I've refactored the parse function, should not depend on ht anymore

map)
"Keymap active when showing inline code suggestions")

;;;###autoload
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we don't need to make this autoload

lsp-inline-completion--overlay)


;;;###autoload
Copy link
Member

@kiennq kiennq Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. If this is not an interactive command, then we don't need to make it autoload as if it's called from another package, we can just (require 'lsp-inline-completion) instead


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

(setq lsp-inline-completion--showing-company
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we want to include company-mode integration here as we can have multiple alternative package provide the same functionality as company-mode, corfu for example.

What will happen if we don't cancel company mode here? The overlay from company-mode will be displayed over the inline completion? Can that be mitigate by making the overlay from inline completion topmost or bottommost?

Also, we can put it as a tip for the user (a simple function that the user can put to their init file) instead? Or even suggest the user to use a different company-mode's front end?
I've been using copilot.el with company-mode and haven't really notice issue so far though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we want to include company-mode integration here as we can have multiple alternative package provide the same functionality as company-mode, corfu for example.

Hmmm I agree that having no way of plugging other completion frontends may be an issue -- corfu users would have to advise the inline completion functions to have a similar level of integration, so not very ideal.

What will happen if we don't cancel company mode here?

It depends on what frontends will be active at the time we display the overlay. With the default company tooltip frontend active, I only managed to get one-liners such as in the video below. It does not obsure the company completion list, but pressing tab or whatever uses the inline completion overlay active map, so this would cause some confusion on the user side.

Kooha-2024-11-27-15-58-38.webm

What happens if company-preview frontend is active? Not sure, probably the inline completion would obscure the preview completely.

If the user uses company-box or company-posframe (my case), then overlay priority makes no difference because company displays completions in a child frame :/

a tip for the user (a simple function that the user can put to their init file) instead?

Good one!

I've refactored the company integration into a minor mode. The user can then set up their code such as in the following snippet:

  (require 'lsp-inline-completion)
  ;; Show the current/total plus keys in the 
  (add-hook 'lsp-inline-completion-shown-hook #'lsp-inline-completion-show-keys)

  ;; Make inline completions play nice with company mode
  (add-hook 'lsp-inline-completion-mode-hook #'lsp-inline-completion-company-integration-mode)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but pressing tab or whatever uses the inline completion overlay active map, so this would cause some confusion on the user side.

Maybe we should bind to other keys for that? I think Vs Code use other key bindings to accept inline completion item compare to the normal completion items as well?
For example, I binded something like this for copilot.el.

;; http://endlessparentheses.com/define-context-aware-keys-in-emacs.html
(defmacro define-conditional-key (keymap key def cond &rest bindings)
  "In KEYMAP, define key sequence KEY as DEF conditionally.
This is like `define-key', except the definition disappears
whenever COND evaluates to nil.
BINDINGS is a list of (key def cond)."
  (declare (indent defun)
           (debug (form form form form &rest sexp)))
  (cl-labels ((partition (n seq)
                (seq-partition seq n)))
    (thread-last
      bindings
      (append `(,key ,def ,cond))
      (partition 3)
      (mapcar (pcase-lambda (`(,key ,def ,cond))
                `(define-key ,keymap ,key
                             '(menu-item
                               ,(format "maybe-%s" (or (car (cdr-safe def)) def))
                               nil
                               :filter (lambda (&optional _)
                                         (if ,cond ,def))))))
      macroexp-progn)))

    (define-conditional-key copilot-completion-map
      (kbd "C-<return>") #'copilot-accept-completion (copilot--overlay-visible)
      (kbd "C-j") #'copilot-accept-completion (copilot--overlay-visible)
      (kbd "M-<return>") #'copilot-accept-completion-by-line (copilot--overlay-visible)
      (kbd "C-n") #'copilot-next-completion (copilot--overlay-visible)
      (kbd "C-p") #'copilot-previous-completion (copilot--overlay-visible))

lsp-mode.el Outdated
@@ -9934,6 +9957,43 @@ string."
(setf window-scroll-functions
(delete #'lsp--update-inlay-hints-scroll-function window-scroll-functions)))))


;; Inline Completions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: If possible we should move this to lsp-inline-completion.el instead.
An auto hook can be added to lsp-inline-completion to autoload this feature if it's on.
Example:

;;;###autoload
(add-hook 'lsp-configure-hook (lambda ()
                                (when (and lsp-auto-configure
                                           lsp-completion-enable)
                                  (lsp-completion--enable))))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's a neat trick!

lsp-mode.el Outdated
"Mode automatically displaying inline completions."
:lighter nil
(cond
((and lsp-inline-completion-mode lsp--buffer-workspaces)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use lsp-on-idle-hook instead? The lsp-inlay-hints-mode is using that and it has been working well so far. It also should fire less frequently compared to on-change-hook as when typing, we're likely don't want to request inline completion anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't think that's a good idea.

I've tested here, and triggering it via lsp-idle-hook means we get suggestions after moving the cursor around -- I don't think a user would expect that (I, for one, don't).

Regarding the change hook being more aggressive than idle: Since lsp's idle timer is set both on post-command-hook and on after-change-hook, I don't see how that could be.

In my implementation, what actually happens in after-change is a cancel-timer and run-with-timer (i.e. timer reschedule). We don't ping the server after each change -- we ping the server after lsp-inline-completion-idle-delay seconds have elapsed without any change in the buffer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay. The Visual Studio will trigger inline completion on mouse move as well while the VS Code seems to only trigger it on buffer modification. Whichever as default is fine for me :)

@kiennq
Copy link
Member

kiennq commented Nov 27, 2024

Thank you @kassick for your contribution. I've left some comments in the PR. The feature looks good to me :)

(cancel-timer lsp-inline-completion--idle-timer))
(let ((buffer (current-buffer)))
(setq lsp-inline-completion--idle-timer
(run-with-timer lsp-inline-completion-idle-delay
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: run-with-idle-timer?


(defun lsp-inline-completion--after-change (&rest _)
(when (and lsp-inline-completion-mode lsp--buffer-workspaces)
(when lsp-inline-completion--idle-timer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we can move this outside of the when check to make sure lsp-inline-completion--idle-timer always be cancelled?

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

(define-minor-mode lsp-inline-completion-mode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to mark this as autoload as well so the add-hook autoload below, when called will know how to trigger this.

(mapconcat 'string-trim-right
(split-string (lsp--render-on-hover-content contents t) "\n")
"\n"))))
(mapconcat 'string-trim-right
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the indentation here correct?

Copy link
Member

@kiennq kiennq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks good now, just a few nit comments you might want to consider for a smooth load of this mode.

lsp-inline-completion--overlay)


(defun lsp-inline-completion-show-keys ()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: is this called from somewhere? Should we make this a command instead of just a function?

(setq lsp-inline-completion--start-point (point))
(lsp-inline-completion-show-overlay))
(unless implicit
(message "No Suggestions!")))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: lsp--info or lsp--warn?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants