244 lines
9.9 KiB
EmacsLisp
244 lines
9.9 KiB
EmacsLisp
|
|
;;; lsp-copilot.el --- lsp-mode client for Copilot -*- lexical-binding: t -*-
|
||
|
|
|
||
|
|
;; Copyright (C) 2024 Rodrigo Virote Kassick
|
||
|
|
;; Copyright (C) 2024-2026 emacs-lsp maintainers
|
||
|
|
|
||
|
|
;; Author: Rodrigo Virote Kassick <kassick@gmail.com>
|
||
|
|
;; Keywords: lsp-mode, generative-ai, code-assistant
|
||
|
|
|
||
|
|
;; This file is not part of GNU Emacs
|
||
|
|
|
||
|
|
;; 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 <https://www.gnu.org/licenses/>.
|
||
|
|
|
||
|
|
;; Commentary:
|
||
|
|
|
||
|
|
;; LSP client for the Copilot Language Server:
|
||
|
|
;; https://www.npmjs.com/package/@github/copilot-language-server
|
||
|
|
|
||
|
|
;; Code:
|
||
|
|
|
||
|
|
(require 'dash)
|
||
|
|
(require 'lsp-mode)
|
||
|
|
(require 's)
|
||
|
|
|
||
|
|
(defgroup lsp-copilot ()
|
||
|
|
"Copilot LSP configuration"
|
||
|
|
:group 'lsp-mode
|
||
|
|
:tag "Copilot LSP"
|
||
|
|
:link '(url-link "https://www.npmjs.com/package/@github/copilot-language-server"))
|
||
|
|
|
||
|
|
(defcustom lsp-copilot-enabled nil
|
||
|
|
"Whether the server should be started to provide completions."
|
||
|
|
:type 'boolean
|
||
|
|
:group 'lsp-copilot)
|
||
|
|
|
||
|
|
(defcustom lsp-copilot-auth-check-delay 5
|
||
|
|
"How much time to wait before checking if the server is properly authenticated.
|
||
|
|
|
||
|
|
Set this value to nil if you do not want for the check to be made."
|
||
|
|
:type '(choice (const :tag "Do not check" nil)
|
||
|
|
(integer :tag "Seconds" 5)))
|
||
|
|
|
||
|
|
(defcustom lsp-copilot-langserver-command-args '("--stdio")
|
||
|
|
"Command to start copilot-langserver."
|
||
|
|
:type '(repeat string)
|
||
|
|
:group 'lsp-copilot)
|
||
|
|
|
||
|
|
(defcustom lsp-copilot-executable "copilot-language-server"
|
||
|
|
"The system-wise executable of lsp-copilot.
|
||
|
|
When this executable is not found, you can still use
|
||
|
|
lsp-install-server to fetch an emacs-local version of the LSP."
|
||
|
|
:type 'string
|
||
|
|
:group 'lsp-copilot)
|
||
|
|
|
||
|
|
(defcustom lsp-copilot-applicable-fn (lambda (&rest _) lsp-copilot-enabled)
|
||
|
|
"A function which returns whether the Copilot is applicable for the buffer.
|
||
|
|
The input are the file name and the major mode of the buffer."
|
||
|
|
:type 'function
|
||
|
|
:group 'lsp-copilot)
|
||
|
|
|
||
|
|
(defcustom lsp-copilot-server-disabled-languages nil
|
||
|
|
"The languages for which the server must not be enabled (initialization setup for Copilot)"
|
||
|
|
:type '(repeat string)
|
||
|
|
:group 'lsp-copilot)
|
||
|
|
|
||
|
|
(defcustom lsp-copilot-server-multi-root t
|
||
|
|
"Whether the Copilot server is started with multi-root."
|
||
|
|
:type 'boolean
|
||
|
|
:group 'lsp-copilot)
|
||
|
|
|
||
|
|
(defcustom lsp-copilot-version "1.270.0"
|
||
|
|
"Copilot version."
|
||
|
|
:type '(choice (const :tag "Latest" nil)
|
||
|
|
(string :tag "Specific Version"))
|
||
|
|
:group 'lsp-copilot)
|
||
|
|
|
||
|
|
(lsp-dependency 'copilot-ls
|
||
|
|
`(:system ,lsp-copilot-executable)
|
||
|
|
'(:npm :package "@github/copilot-language-server"
|
||
|
|
:path "copilot-language-server"
|
||
|
|
:version lsp-copilot-version))
|
||
|
|
|
||
|
|
(defun lsp-copilot--find-active-workspaces ()
|
||
|
|
"Returns a list of lsp-copilot workspaces"
|
||
|
|
(-some->> (lsp-session)
|
||
|
|
(lsp--session-workspaces)
|
||
|
|
(--filter (member (lsp--client-server-id (lsp--workspace-client it))
|
||
|
|
'(copilot-ls copilot-ls-tramp)))))
|
||
|
|
|
||
|
|
(defun lsp-copilot--authenticated-as ()
|
||
|
|
"Returns nil when not authorized; otherwise, the user name"
|
||
|
|
(-if-let (workspace (--some (lsp-find-workspace it (buffer-file-name))
|
||
|
|
'(copilot-ls copilot-ls-tramp)))
|
||
|
|
(-if-let (checkStatusResponse (with-lsp-workspace workspace
|
||
|
|
(lsp-request "checkStatus" '(:dummy "dummy"))))
|
||
|
|
(-let* (((&copilot-ls:CheckStatusResponse? :status :user) checkStatusResponse))
|
||
|
|
(unless (s-present-p status)
|
||
|
|
(error "No status in response %S" checkStatusResponse))
|
||
|
|
;; Result:
|
||
|
|
(when (s-equals-p status "OK")
|
||
|
|
user))
|
||
|
|
(error "No response from the LSP server"))
|
||
|
|
(error "No lsp-copilot workspace found!")))
|
||
|
|
|
||
|
|
;;;###autoload
|
||
|
|
(defun lsp-copilot-check-status ()
|
||
|
|
"Checks the status of the Copilot Server"
|
||
|
|
(interactive)
|
||
|
|
|
||
|
|
(condition-case err
|
||
|
|
(progn
|
||
|
|
(let ((user (lsp-copilot--authenticated-as)))
|
||
|
|
(if user
|
||
|
|
(message "Authenticated as %s" user)
|
||
|
|
(user-error "Not Authenticated"))))
|
||
|
|
(t (user-error "Error checking status: %s" err))))
|
||
|
|
|
||
|
|
|
||
|
|
;;;###autoload
|
||
|
|
(defun lsp-copilot-login ()
|
||
|
|
"Log in with Copilot.
|
||
|
|
|
||
|
|
This function is automatically called during the client initialization if needed"
|
||
|
|
(interactive)
|
||
|
|
|
||
|
|
(-when-let (workspace (--some (lsp-find-workspace it) '(copilot-ls copilot-ls-tramp)))
|
||
|
|
(with-lsp-workspace workspace
|
||
|
|
(-when-let* ((response (lsp-request "signInInitiate" '(:dummy "dummy"))))
|
||
|
|
(-let (((&copilot-ls:SignInInitiateResponse? :status :user-code :verification-uri :user) response))
|
||
|
|
|
||
|
|
;; Bail if already signed in
|
||
|
|
(cond
|
||
|
|
((s-equals-p status "AlreadySignedIn")
|
||
|
|
(lsp--info "Copilot :: Already signed in as %s" user))
|
||
|
|
((yes-or-no-p "Copilot requires you to log into your GitHub account. Proceed now?")
|
||
|
|
(if (display-graphic-p)
|
||
|
|
(progn
|
||
|
|
(gui-set-selection 'CLIPBOARD user-code)
|
||
|
|
(read-from-minibuffer (format "Your one-time code %s is copied. Press \
|
||
|
|
ENTER to open GitHub in your browser. If your browser does not open \
|
||
|
|
automatically, browse to %s." user-code verification-uri))
|
||
|
|
(browse-url verification-uri)
|
||
|
|
(read-from-minibuffer "Press ENTER if you finish authorizing."))
|
||
|
|
;; Console:
|
||
|
|
(read-from-minibuffer (format "First copy your one-time code: %s. Press ENTER to continue." user-code))
|
||
|
|
(read-from-minibuffer (format "Please open %s in your browser. Press ENTER if you finish authorizing." verification-uri)))
|
||
|
|
|
||
|
|
(lsp--info "Verifying...")
|
||
|
|
(-let* ((confirmResponse (lsp-request "signInConfirm" (list :userCode user-code)))
|
||
|
|
((&copilot-ls:SignInConfirmResponse? :status :user) confirmResponse))
|
||
|
|
(when (s-equals-p status "NotAuthorized")
|
||
|
|
(user-error "User %s is not authorized" user))
|
||
|
|
(lsp--info "User %s is authorized: %s" user status))
|
||
|
|
|
||
|
|
;; Do we need to confirm?
|
||
|
|
(-let* ((checkStatusResponse (lsp-request "checkStatus" '(:dummy "dummy")))
|
||
|
|
((&copilot-ls:CheckStatusResponse? :status :user) checkStatusResponse))
|
||
|
|
(when (s-equals-p status "NotAuthorized")
|
||
|
|
(user-error "User %s is not authorized" user))
|
||
|
|
|
||
|
|
(lsp--info "Authenticated as %s" user)))
|
||
|
|
(t
|
||
|
|
(message "Aborting Copilot login. To avoid being asked again, customize `lsp-copilot-enabled'"))))))))
|
||
|
|
|
||
|
|
(defun lsp-copilot-logout ()
|
||
|
|
"Logout from Copilot."
|
||
|
|
(interactive)
|
||
|
|
(-when-let (workspace (--some (lsp-find-workspace it) '(copilot-ls copilot-ls-tramp)))
|
||
|
|
(with-lsp-workspace workspace
|
||
|
|
(lsp-request "signOut" '(:dummy "dummy"))
|
||
|
|
(lsp--info "Logged out."))))
|
||
|
|
|
||
|
|
(defun lsp-copilot--server-initialization-options ()
|
||
|
|
;; Trying to replicate Copilot.vim initialization here ...
|
||
|
|
(list :editorInfo (list :name "emacs" :version emacs-version)
|
||
|
|
:editorPluginInfo (list :name "lsp-copilot" :version (lsp-package-version))
|
||
|
|
:editorConfig (list :enableAutoCompletions lsp-copilot-enabled
|
||
|
|
:disabledLanguages lsp-copilot-server-disabled-languages)
|
||
|
|
:name "emacs"
|
||
|
|
:version "0.1.0"))
|
||
|
|
|
||
|
|
(defun lsp-copilot--mcp-tools-notification (_ params)
|
||
|
|
"Reacts to the copilot/mcpTools notification"
|
||
|
|
(-let* (((&copilot-ls:McpToolsNotification :servers) params)
|
||
|
|
(tools-str (s-join ", " servers)))
|
||
|
|
(when (s-present? tools-str)
|
||
|
|
(lsp--info "Copilot: Available MCP Tools: %s" tools-str))))
|
||
|
|
|
||
|
|
(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
|
||
|
|
(lambda ()
|
||
|
|
(condition-case err
|
||
|
|
(unless (lsp-copilot--authenticated-as)
|
||
|
|
(lsp-copilot-login))
|
||
|
|
(t (lsp--error "Could not authenticate with Copilot: %s" (error-message-string err)))))))
|
||
|
|
t)
|
||
|
|
|
||
|
|
;; Server installed by emacs
|
||
|
|
(lsp-register-client
|
||
|
|
(make-lsp-client
|
||
|
|
:server-id 'copilot-ls
|
||
|
|
:new-connection (lsp-stdio-connection
|
||
|
|
(lambda () `(,(lsp-package-path 'copilot-ls) ,@lsp-copilot-langserver-command-args))
|
||
|
|
)
|
||
|
|
:activation-fn lsp-copilot-applicable-fn
|
||
|
|
:multi-root lsp-copilot-server-multi-root
|
||
|
|
:priority -2
|
||
|
|
:add-on? t
|
||
|
|
:completion-in-comments? t
|
||
|
|
:initialization-options #'lsp-copilot--server-initialization-options
|
||
|
|
:initialized-fn #'lsp-copilot--server-initialized-fn
|
||
|
|
: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)))
|
||
|
|
("featureFlagsNotification" #'ignore)
|
||
|
|
("statusNotification" #'ignore)
|
||
|
|
("didChangeStatus" #'ignore)
|
||
|
|
("copilot/mcpTools" #'lsp-copilot--mcp-tools-notification)
|
||
|
|
("window/logMessage" #'lsp--window-log-message)
|
||
|
|
("conversation/preconditionsNotification" #'ignore))))
|
||
|
|
|
||
|
|
(lsp-consistency-check lsp-copilot)
|
||
|
|
|
||
|
|
(provide 'lsp-copilot)
|