|
| 1 | +;;; lsp-copilot.el --- lsp-mode client for copilot -*- lexical-binding: t -*- |
| 2 | + |
| 3 | +;; Copyright (C) 2024 Rodrigo Virote Kassick |
| 4 | + |
| 5 | +;; Author: Rodrigo Virote Kassick <[email protected]> |
| 6 | +;; Keywords: lsp-mode, generative-ai, code-assistant |
| 7 | + |
| 8 | +;; This file is not part of GNU Emacs |
| 9 | + |
| 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 | +;; LSP client for the copilot node server -- https://www.npmjs.com/package/copilot-node-server |
| 26 | + |
| 27 | +;; Package-Requires: (lsp-mode secrets s compile dash cl-lib request company) |
| 28 | + |
| 29 | +;; Code: |
| 30 | + |
| 31 | +(require 'dash) |
| 32 | +(require 'lsp-mode) |
| 33 | +(require 's) |
| 34 | + |
| 35 | +(defgroup lsp-copilot () |
| 36 | + "Copilot LSP configuration" |
| 37 | + :group 'lsp-mode |
| 38 | + :tag "Copilot LSP" |
| 39 | + :link '(url-link "https://www.npmjs.com/package/copilot-node-server")) |
| 40 | + |
| 41 | +(defcustom lsp-copilot-enabled t |
| 42 | + "Whether the server should be started to provide completions." |
| 43 | + :type 'boolean |
| 44 | + :group 'lsp-copilot) |
| 45 | + |
| 46 | +(defcustom lsp-copilot-langserver-command-args '("--stdio") |
| 47 | + "Command to start copilot-langserver." |
| 48 | + :type '(repeat string) |
| 49 | + :group 'lsp-copilot) |
| 50 | + |
| 51 | +(defcustom lsp-copilot-executable "copilot-lsp" |
| 52 | + "The system-wise executable of lsp-copilot. |
| 53 | +When this executable is not found, you can stil use |
| 54 | +lsp-install-server to fetch an emacs-local version of the LSP." |
| 55 | + :type 'string |
| 56 | + :group 'lsp-copilot) |
| 57 | + |
| 58 | +(defcustom lsp-copilot-applicable-fn (-const t) |
| 59 | + "A function which returns whether the copilot is applicable for the buffer. |
| 60 | +The input are the file name and the major mode of the buffer." |
| 61 | + :type 'function |
| 62 | + :group 'lsp-copilot) |
| 63 | + |
| 64 | +(defcustom lsp-copilot-server-disabled-languages nil |
| 65 | + "The languages for which the server must not be enabled (initialization setup for copilot)" |
| 66 | + :type '(repeat string) |
| 67 | + :group 'lsp-copilot) |
| 68 | + |
| 69 | +(defcustom lsp-copilot-server-multi-root t |
| 70 | + "Whether the copilot server is started with multi-root." |
| 71 | + :type 'boolean |
| 72 | + :group 'lsp-copilot) |
| 73 | + |
| 74 | +(defcustom lsp-copilot-version "1.41.0" |
| 75 | + "Copilot version." |
| 76 | + :type '(choice (const :tag "Latest" nil) |
| 77 | + (string :tag "Specific Version")) |
| 78 | + :group 'lsp-copilot) |
| 79 | + |
| 80 | +(lsp-dependency 'copilot-ls |
| 81 | + `(:system ,lsp-copilot-executable) |
| 82 | + '(:npm :package "copilot-node-server" |
| 83 | + :path "copilot-node-server" |
| 84 | + :version lsp-copilot-version)) |
| 85 | + |
| 86 | +(defun lsp-copilot--find-active-workspaces () |
| 87 | + "Returns a list of lsp-copilot workspaces" |
| 88 | + (-some->> (lsp-session) |
| 89 | + (lsp--session-workspaces) |
| 90 | + (--filter (member (lsp--client-server-id (lsp--workspace-client it)) |
| 91 | + '(copilot-ls copilot-ls-tramp))))) |
| 92 | + |
| 93 | +(defun lsp-copilot--authenticated-as () |
| 94 | + "Returns nil when not authorized; otherwise, the user name" |
| 95 | + (-if-let (workspace (--some (lsp-find-workspace it (buffer-file-name)) |
| 96 | + '(copilot-ls copilot-ls-tramp))) |
| 97 | + (-if-let (checkStatusResponse (with-lsp-workspace workspace |
| 98 | + (lsp-request "checkStatus" '(:dummy "dummy")))) |
| 99 | + (-let* (((&copilot-ls:CheckStatusResponse? :status :user) checkStatusResponse)) |
| 100 | + (unless (s-present-p status) |
| 101 | + (error "No status in response %S" checkStatusResponse)) |
| 102 | + ;; Result: |
| 103 | + (when (s-equals-p status "OK") |
| 104 | + user)) |
| 105 | + (error "No response from the LSP server")) |
| 106 | + (error "No lsp-copilot workspace found!"))) |
| 107 | + |
| 108 | +;;;###autoload |
| 109 | +(defun lsp-copilot-check-status () |
| 110 | + "Checks the status of the Copilot Server" |
| 111 | + (interactive) |
| 112 | + |
| 113 | + (condition-case err |
| 114 | + (progn |
| 115 | + (let ((user (lsp-copilot--authenticated-as))) |
| 116 | + (if user |
| 117 | + (message "Authenticated as %s" user) |
| 118 | + (user-error "Not Authenticated")))) |
| 119 | + (t (user-error "Error checking status: %s" err)))) |
| 120 | + |
| 121 | + |
| 122 | +;;;###autoload |
| 123 | +(defun lsp-copilot-login () |
| 124 | + "Log in with copilot. |
| 125 | +
|
| 126 | +This function is automatically called during the client initialization if needed" |
| 127 | + (interactive) |
| 128 | + |
| 129 | + (-when-let (workspace (--some (lsp-find-workspace it) '(copilot-ls copilot-ls-tramp))) |
| 130 | + (with-lsp-workspace workspace |
| 131 | + (-when-let* ((response (lsp-request "signInInitiate" '(:dummy "dummy")))) |
| 132 | + (-let (((&copilot-ls:SignInInitiateResponse? :status :user-code :verification-uri :user) response)) |
| 133 | + |
| 134 | + ;; Bail if already signed in |
| 135 | + (when (s-equals-p status "AlreadySignedIn") |
| 136 | + (lsp-message "Copilot :: Already signed in as %s" user)) |
| 137 | + |
| 138 | + (if (display-graphic-p) |
| 139 | + (progn |
| 140 | + (gui-set-selection 'CLIPBOARD user-code) |
| 141 | + (read-from-minibuffer (format "Your one-time code %s is copied. Press \ |
| 142 | +ENTER to open GitHub in your browser. If your browser does not open \ |
| 143 | +automatically, browse to %s." user-code verification-uri)) |
| 144 | + (browse-url verification-uri) |
| 145 | + (read-from-minibuffer "Press ENTER if you finish authorizing.")) |
| 146 | + ;; Console: |
| 147 | + (read-from-minibuffer (format "First copy your one-time code: %s. Press ENTER to continue." user-code)) |
| 148 | + (read-from-minibuffer (format "Please open %s in your browser. Press ENTER if you finish authorizing." verification-uri))) |
| 149 | + |
| 150 | + (lsp-message "Verifying...") |
| 151 | + (-let* ((confirmResponse (lsp-request "signInConfirm" (list :userCode user-code))) |
| 152 | + ((&copilot-ls:SignInConfirmResponse? :status :user) confirmResponse)) |
| 153 | + (when (s-equals-p status "NotAuthorized") |
| 154 | + (user-error "User %s is not authorized" user)) |
| 155 | + (lsp-message "User %s is authorized: %s" user status)) |
| 156 | + |
| 157 | + ;; Do we need to confirm? |
| 158 | + (-let* ((checkStatusResponse (lsp-request "checkStatus" '(:dummy "dummy"))) |
| 159 | + ((&copilot-ls:CheckStatusResponse? :status :user) checkStatusResponse)) |
| 160 | + (when (s-equals-p status "NotAuthorized") |
| 161 | + (user-error "User %s is not authorized" user)) |
| 162 | + |
| 163 | + (lsp-message "Authenticated as %s" user))))))) |
| 164 | + |
| 165 | +(defun lsp-copilot-logout () |
| 166 | + "Logout from Copilot." |
| 167 | + (interactive) |
| 168 | + (-when-let (workspace (--some (lsp-find-workspace it) '(copilot-ls copilot-ls-tramp))) |
| 169 | + (with-lsp-workspace workspace |
| 170 | + (lsp-request "signOut" '(:dummy "dummy")) |
| 171 | + (lsp--info "Logged out.")))) |
| 172 | + |
| 173 | +(defun lsp-copilot--server-initialization-options () |
| 174 | + ;; Trying to replicate Copilot.vim initialization here ... |
| 175 | + (list :editorInfo (list :name "emacs" :version emacs-version) |
| 176 | + :editorPluginInfo (list :name "lsp-copilot" :version (lsp-package-version)) |
| 177 | + :editorConfig (list :enableAutoCompletions lsp-copilot-enabled |
| 178 | + :disabledLanguages lsp-copilot-server-disabled-languages) |
| 179 | + :name "emacs" |
| 180 | + :version "0.1.0")) |
| 181 | + |
| 182 | +(defun lsp-copilot--server-initialized-fn (workspace) |
| 183 | + ;; Patch capabilities -- server may respond with an empty dict. In plist, |
| 184 | + ;; this would become nil |
| 185 | + (let ((caps (lsp--workspace-server-capabilities workspace))) |
| 186 | + (lsp:set-server-capabilities-inline-completion-provider? caps t)) |
| 187 | + |
| 188 | + (unless (lsp-copilot--authenticated-as) |
| 189 | + (lsp-copilot-login))) |
| 190 | + |
| 191 | +;; Server installed by emacs |
| 192 | +(lsp-register-client |
| 193 | + (make-lsp-client |
| 194 | + :server-id 'copilot-ls |
| 195 | + :new-connection (lsp-stdio-connection |
| 196 | + (lambda () `(,(lsp-package-path 'copilot-ls) ,@lsp-copilot-langserver-command-args)) |
| 197 | + ) |
| 198 | + :activation-fn lsp-copilot-applicable-fn |
| 199 | + :multi-root lsp-copilot-server-multi-root |
| 200 | + :priority -2 |
| 201 | + :add-on? t |
| 202 | + :completion-in-comments? t |
| 203 | + :initialization-options #'lsp-copilot--server-initialization-options |
| 204 | + :initialized-fn #'lsp-copilot--server-initialized-fn |
| 205 | + :download-server-fn (lambda (_client callback error-callback _update?) |
| 206 | + (lsp-package-ensure 'copilot-ls callback error-callback)) |
| 207 | + :notification-handlers (lsp-ht |
| 208 | + ("$/progress" (lambda (&rest args) (lsp-message "$/progress with %S" args))) |
| 209 | + ("featureFlagsNotification" #'ignore) |
| 210 | + ("statusNotification" #'ignore) |
| 211 | + ("window/logMessage" #'lsp--window-log-message) |
| 212 | + ("conversation/preconditionsNotification" #'ignore)))) |
| 213 | + |
| 214 | +(lsp-consistency-check lsp-copilot) |
| 215 | + |
| 216 | +(provide 'lsp-copilot) |
0 commit comments