dotfiles/emacs/.emacs.d/elpa/lsp-mode-20260716.755/lsp-copilot.el

243 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)