Skip to content

Feat: copilot panel #4844

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Eask
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
(depends-on "markdown-mode")
(depends-on "lv")
(depends-on "eldoc")
(depends-on "uuidgen")

(development
(depends-on "flycheck")
Expand Down
222 changes: 219 additions & 3 deletions clients/lsp-copilot.el
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
;; LSP client for the Copilot Language Server:
;; https://www.npmjs.com/package/@github/copilot-language-server

;; Package-Requires: (lsp-mode secrets s compile dash cl-lib request company)
;; Package-Requires: (lsp-mode secrets s compile dash cl-lib request uuidgen)

;; Code:

(require 'dash)
(require 'lsp-mode)
(require 's)
(require 'uuidgen)
(require 'lsp-inline-completion)

(defgroup lsp-copilot ()
"Copilot LSP configuration"
Expand Down Expand Up @@ -91,6 +93,215 @@ The input are the file name and the major mode of the buffer."
:path "copilot-language-server"
:version lsp-copilot-version))


;;; Panel Completion

(declare-function company--active-p "ext:company")
(declare-function company-cancel "ext:company" (&optional result))
(declare-function org-forward-heading-same-level "ext:org" (arg &optional invisible-ok))
(declare-function org-backward-heading-same-level "ext:org" (arg &optional invisible-ok))
(declare-function org-down-element "ext:org")
(defvar-local lsp-copilot-panel-completion-items nil
"A list of completion items returned by the Panel Completion call")

(defvar-local lsp-copilot-panel-completion-token nil
"The per-request token")

(defvar-local lsp-copilot-panel-modification-tick nil
"Modification tick when the panel was called")

(defun lsp-copilot--panel-accept-item (completing-buffer-name original-tick item)
(when (buffer-live-p (get-buffer completing-buffer-name))
(with-current-buffer completing-buffer-name
(unless (= original-tick (buffer-chars-modified-tick))
(user-error "Can not accept the suggestion on a modified buffer. Try copying it"))

(-let* (((&copilot-ls:PanelCompletionItem? :insert-text :range? :command?) item)
((start . end) (when range?
(-let (((&RangeToPoint :start :end) range?)) (cons start end)))))

(unless (and start end)
(error "Server did not provide a range -- Can not insert"))

(lsp-with-undo-amalgamate
(delete-region start end)
(goto-char start)
(insert insert-text))

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

(defun lsp-copilot--panel-accept-suggestion-at-point ()
(interactive)
(let ((completing-buffer-name (get-text-property (point) 'lsp-panel-completing-buffer-name))
(original-tick (get-text-property (point) 'lsp-original-tick))
(item (get-text-property (point) 'lsp-panel-item)))
(lsp-copilot--panel-accept-item completing-buffer-name original-tick item)
(quit-window)))

(defun lsp-copilot--panel-accept-button-click (b)
(when-let* ((item (overlay-get b 'lsp-panel-item))
(completing-buffer-name (overlay-get b 'lsp-panel-completing-buffer-name))
(original-tick (overlay-get b 'lsp-original-tick)))
(lsp-copilot--panel-accept-item completing-buffer-name original-tick item)

(quit-window)))

(defun lsp-copilot--panel-copy-button-click (b)

(kill-new (lsp:copilot-ls-panel-completion-item-insert-text (overlay-get b 'lsp-panel-item)))
(quit-window))

(defun lsp-copilot--panel-forward-suggestion (arg)
(interactive "p")
(org-forward-heading-same-level arg)
(recenter-top-bottom 0)
(org-down-element))

(defun lsp-copilot--panel-backward-suggestion (arg)
(interactive "p")
(org-backward-heading-same-level arg)
(recenter-top-bottom 0)
(org-down-element))

(define-derived-mode lsp-copilot-panel-buffer-mode org-mode "CopilotPanel"
"A major mode for completions "
:group 'lsp-copilot)


(let ((key-bindings '(("C-n" . lsp-copilot--panel-forward-suggestion)
("C-p" . lsp-copilot--panel-backward-suggestion)
("C-<return>" . lsp-copilot--panel-accept-suggestion-at-point)
("q" . quit-window))))
(dolist (binding key-bindings)
(define-key lsp-copilot-panel-buffer-mode-map (kbd (car binding)) (cdr binding))))


(defcustom lsp-copilot-panel-display-fn #'pop-to-buffer
"Function used to display the panel completions buffer"
:type 'function
:group 'lsp-copilot)


(defun lsp-copilot--panel-display-buffer (completing-buffer-name items original-tick)
"Builds and display the panel buffer"
(if (lsp-inline-completion--active-p)
(progn
(lsp-inline-completion-cancel)))

(when (and (bound-and-true-p company-mode)
(company--active-p))
(company-cancel))

(let ((buf (get-buffer-create (format "*lsp-copilot-panel-results-%s*" completing-buffer-name) )))
(with-current-buffer buf
(read-only-mode -1)
(erase-buffer)
(fundamental-mode)

;; (insert "#+STARTUP: noindent\n\n")
(setq-local org-startup-indented nil)
(cl-loop for item in items
for i from 1
do
(-let* ((start-point (point))
((&copilot-ls:PanelCompletionItem? :insert-text) item)
(heading (format "* Solution %d" i))
(src (format "#+begin_src %s\n%s\n#+end_src\n" "python" insert-text)))

(insert heading)
(insert ?\n ?\n)
(insert-button "Accept"
'action #'lsp-copilot--panel-accept-button-click
'lsp-original-tick original-tick
'lsp-panel-item item
'lsp-panel-completing-buffer-name completing-buffer-name)

(insert ?\s)

(insert-button "Copy"
'action #'lsp-copilot--panel-copy-button-click
'lsp-original-tick original-tick
'lsp-panel-item item
'lsp-panel-completing-buffer-name completing-buffer-name)

(insert ?\n)
(insert src)
(insert ?\n ?\n)

(put-text-property start-point (point) 'lsp-original-tick original-tick)
(put-text-property start-point (point) 'lsp-panel-item item)
(put-text-property start-point (point) 'lsp-panel-completing-buffer-name completing-buffer-name)))

(lsp-delete-all-space t)
(lsp-copilot-panel-buffer-mode)
(read-only-mode +1)

(goto-char (point-min))
(org-down-element))

(if (get-buffer-window completing-buffer-name 'visible)
(progn
(select-window (get-buffer-window completing-buffer-name 'visible))
(funcall lsp-copilot-panel-display-fn buf))
(user-error "The original buffer for completions was not active; not showing panel"))))


(defun lsp-copilot-panel-display-buffer ()
"Displays a completion panel with the items collected by the last call of lsp-copilot-panel-completion"
(interactive)

(if lsp-copilot-panel-completion-items
(lsp-copilot--panel-display-buffer (buffer-name) lsp-copilot-panel-completion-items lsp-copilot-panel-modification-tick)
(lsp--error "No completions to display")))

(defun lsp-copilot--panel-completions-progress-handler (_ params)
(-let* (((&ProgressParams :token :value) params)
((action completing-buffer-name panel-completion-token) (s-split " /// " token)))
(pcase action
;; copilot sends results in the report
("PANEL-PARTIAL-RESULT"
(when (and (lsp-copilot-ls-panel-completion-items? value)
(buffer-live-p (get-buffer completing-buffer-name)))
(with-current-buffer completing-buffer-name
(when (string-equal panel-completion-token lsp-copilot-panel-completion-token)
(setq-local lsp-copilot-panel-completion-items
(nconc lsp-copilot-panel-completion-items
(seq-into (lsp:copilot-ls-panel-completion-items-items value) 'list)))))))
("PANEL-WORK-DONE"
(when (and (lsp-work-done-progress-end? value)
(buffer-live-p (get-buffer completing-buffer-name))
(string-equal (lsp:work-done-progress-end-kind value) "end"))
(with-current-buffer completing-buffer-name
(when (string-equal panel-completion-token lsp-copilot-panel-completion-token)
(lsp-copilot-panel-display-buffer))))))))

(defun lsp-copilot-panel-completion ()
"Use a Completion Panel to provide suggestions at point"
(interactive)

(lsp-inline-completion-inhibit-idle-completions)
(lsp-inline-completion-cancel-timer)

(let* ((token (uuidgen-1))
(partial-token (format "PANEL-PARTIAL-RESULT /// %s /// %s" (buffer-name) token))
(done-token (format "PANEL-WORK-DONE /// %s /// %s" (buffer-name) token))
(params (lsp-make-copilot-ls-panel-completion-params :text-document (lsp--text-document-identifier)
:position (lsp--cur-position)
:partial-result-token partial-token
:work-done-token done-token)))
(setq-local lsp-copilot-panel-modification-tick (buffer-chars-modified-tick))
(setq-local lsp-copilot-panel-completion-token token)
(setq-local lsp-copilot-panel-completion-items nil)

;; call the complation and do not wait for any result -- the completions
;; will be provided via $/progress notifications
(lsp-request-async "textDocument/copilotPanelCompletion" params #'ignore)))


;;; LSP Client

(defun lsp-copilot--find-active-workspaces ()
"Returns a list of lsp-copilot workspaces"
(-some->> (lsp-session)
Expand Down Expand Up @@ -190,13 +401,18 @@ automatically, browse to %s." user-code verification-uri))
:name "emacs"
:version "0.1.0"))

(defun lsp-copilot--progress-callback (workspace params)
(when lsp-progress-function
(funcall lsp-progress-function workspace params))

(lsp-copilot--panel-completions-progress-handler workspace params))

(defun lsp-copilot--server-initialized-fn (workspace)
;; Patch capabilities -- server may respond with an empty dict. In plist,
;; this would become nil
(let ((caps (lsp--workspace-server-capabilities workspace)))
(lsp:set-server-capabilities-inline-completion-provider? caps t))


(when lsp-copilot-auth-check-delay
(run-at-time lsp-copilot-auth-check-delay
nil
Expand Down Expand Up @@ -224,7 +440,7 @@ automatically, browse to %s." user-code verification-uri))
:download-server-fn (lambda (_client callback error-callback _update?)
(lsp-package-ensure 'copilot-ls callback error-callback))
:notification-handlers (lsp-ht
("$/progress" (lambda (&rest args) (lsp-message "$/progress with %S" args)))
("$/progress" #'lsp-copilot--progress-callback)
("featureFlagsNotification" #'ignore)
("statusNotification" #'ignore)
("didChangeStatus" #'ignore)
Expand Down
44 changes: 39 additions & 5 deletions lsp-inline-completion.el
Original file line number Diff line number Diff line change
Expand Up @@ -407,25 +407,59 @@ lsp-inline-completion-mode is active."
(defvar-local lsp-inline-completion--idle-timer nil
"The idle timer used by lsp-inline-completion-mode.")

(defvar-local lsp-inline-completion--inhibit-idle-timer nil
"Flag to indicate we do not want the timer to show inline completions. Reset on change.")

;;;###autoload
(defun lsp-inline-completion-inhibit-idle-completions ()
(setq lsp-inline-completion--inhibit-idle-timer t))

;;;###autoload
(defun lsp-inline-completion-uninhibit-idle-completions ()
(setq lsp-inline-completion--inhibit-idle-timer nil))

;;;###autoload
(defun lsp-inline-completion-is-inhibited ()
lsp-inline-completion--inhibit-idle-timer)

;;;###autoload
(defun lsp-inline-completion-cancel-timer ()
"Cancels the completion idle timer, if set"
(when lsp-inline-completion--idle-timer
(cancel-timer lsp-inline-completion--idle-timer)
(setq lsp-inline-completion--idle-timer nil)))

;;;###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 'after-change-functions #'lsp-inline-completion--uninhibit-on-change nil t)
(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-timer)

(lsp-inline-completion-cancel)

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

(defun lsp-inline-completion--uninhibit-on-change (&rest _)
"Resets the uninhibit flag. "

;; Must be done in after-change-functions instead of lsp-on-change-hook,
;; because LSP's hook happens after lsp-idle-delay. If the user calls some
;; function that sets the inhibit flag to t *before* the idle delay, we may
;; end up overriding the flag."
(lsp-inline-completion-uninhibit-idle-completions))

(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)
(when (and (not (lsp-inline-completion-is-inhibited))
(buffer-live-p original-buffer)
(eq (current-buffer) original-buffer)
(eq (point) original-point)
(--none? (funcall it) lsp-inline-completion-inhibit-predicates))
Expand All @@ -439,8 +473,7 @@ lsp-inline-completion-mode is active."
;; 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))
(lsp-inline-completion-cancel-timer)

(when (and lsp-inline-completion-mode lsp--buffer-workspaces)
(let ((original-buffer (plist-get lsp--after-change-vals :buffer))
Expand All @@ -457,6 +490,7 @@ lsp-inline-completion-mode is active."
(when (and lsp-inline-completion-enable
(lsp-feature? "textDocument/inlineCompletion"))
(lsp-inline-completion-mode))))


;; Company default integration

Expand Down
Loading
Loading