Skip to content

Commit 83da5b6

Browse files
kassickkiennq
andauthored
Feature/lsp copilot (#4635)
* fix: use lsp-error in case of errors during inline completion * feat: lsp-copilot client for copilot-node-server * refactor: move copilot interfaces to lsp-protocol * refactor: use copilot-ls / copilot-ls-remote as server ids * refactor: remove unused symbols * chore: no need for explicit remote server Now the lsp-controlled copilot npm install path contains a bin with a copilot-node-server -- we no longer need to go out of our way to find the language server js and execute it with node * fix: make copilot-ls works with plist * chore: editor plugin version is package version of lsp-mode * chore: allow nil in copilot version * fix: auto-registered remote variant is called -tramp * refactor: move copilot-specific capabilities kludge to lsp-copilot.el --------- Co-authored-by: Kien Nguyen <[email protected]>
1 parent 7d8f232 commit 83da5b6

File tree

6 files changed

+270
-29
lines changed

6 files changed

+270
-29
lines changed

clients/lsp-copilot.el

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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)

docs/lsp-clients.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@
116116
"lsp-install-server": "camells",
117117
"debugger": "Not available"
118118
},
119+
{
120+
"name": "copilot",
121+
"full-name": "Github Copilot",
122+
"server-name": "copilot-node-server",
123+
"server-url": "https://www.npmjs.com/package/copilot-node-server",
124+
"installation-url": "https://www.npmjs.com/package/copilot-node-server",
125+
"debugger": "Not available"
126+
},
119127
{
120128
"name": "credo",
121129
"full-name": "Credo",

lsp-inline-completion.el

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -372,22 +372,24 @@ text range that was updated by the completion"
372372
(unless implicit
373373
(lsp--spinner-start) )
374374

375-
(unwind-protect
376-
(if-let* ((resp (lsp-request-while-no-input "textDocument/inlineCompletion"
377-
(lsp-inline-completion--params implicit)))
378-
(items (lsp-inline-completion--parse-items resp)))
379-
380-
(progn
381-
(lsp-inline-completion--clear-overlay)
382-
(setq lsp-inline-completion--items items)
383-
(setq lsp-inline-completion--current 0)
384-
(setq lsp-inline-completion--start-point (point))
385-
(lsp-inline-completion-show-overlay))
375+
(condition-case err
376+
(unwind-protect
377+
(if-let* ((resp (lsp-request-while-no-input "textDocument/inlineCompletion"
378+
(lsp-inline-completion--params implicit)))
379+
(items (lsp-inline-completion--parse-items resp)))
380+
381+
(progn
382+
(lsp-inline-completion--clear-overlay)
383+
(setq lsp-inline-completion--items items)
384+
(setq lsp-inline-completion--current 0)
385+
(setq lsp-inline-completion--start-point (point))
386+
(lsp-inline-completion-show-overlay))
387+
(unless implicit
388+
(lsp--info "No Suggestions!")))
389+
;; Clean up
386390
(unless implicit
387-
(lsp--info "No Suggestions!")))
388-
;; Clean up
389-
(unless implicit
390-
(lsp--spinner-stop))))
391+
(lsp--spinner-stop)))
392+
(t (lsp--error "Couldnot fetch completions: %s" err))))
391393

392394

393395
;; Inline Completion Mode

lsp-mode.el

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ As defined by the Language Server Protocol 3.16."
176176
'( ccls lsp-actionscript lsp-ada lsp-angular lsp-ansible lsp-asm lsp-astro
177177
lsp-autotools lsp-awk lsp-bash lsp-beancount lsp-bufls lsp-clangd
178178
lsp-clojure lsp-cmake lsp-cobol lsp-credo lsp-crystal lsp-csharp lsp-css
179-
lsp-cucumber lsp-cypher lsp-d lsp-dart lsp-dhall lsp-docker lsp-dockerfile
179+
lsp-copilot lsp-cucumber lsp-cypher lsp-d lsp-dart lsp-dhall lsp-docker lsp-dockerfile
180180
lsp-earthly lsp-elixir lsp-elm lsp-emmet lsp-erlang lsp-eslint lsp-fortran lsp-futhark
181181
lsp-fsharp lsp-gdscript lsp-gleam lsp-glsl lsp-go lsp-golangci-lint lsp-grammarly
182182
lsp-graphql lsp-groovy lsp-hack lsp-haskell lsp-haxe lsp-idris lsp-java
@@ -3824,7 +3824,8 @@ disappearing, unset all the variables related to it."
38243824
(versionSupport . t)))
38253825
(diagnostic . ((dynamicRegistration . :json-false)
38263826
(relatedDocumentSupport . :json-false)))
3827-
(linkedEditingRange . ((dynamicRegistration . t)))))
3827+
(linkedEditingRange . ((dynamicRegistration . t)))
3828+
(inlineCompletion . ())))
38283829
(window . ((workDoneProgress . t)
38293830
(showDocument . ((support . t))))))
38303831
custom-capabilities))
@@ -5105,6 +5106,7 @@ Applies on type formatting."
51055106
(-lambda ((mode-or-pattern . language))
51065107
(cond
51075108
((and (stringp mode-or-pattern)
5109+
(buffer-file-name)
51085110
(s-matches? mode-or-pattern (buffer-file-name)))
51095111
language)
51105112
((eq mode-or-pattern major-mode) language))))
@@ -7950,12 +7952,13 @@ SESSION is the active session."
79507952
(apply 'vector)
79517953
(list :workspaceFolders))))
79527954
(-lambda ((&InitializeResult :capabilities))
7953-
;; we know that Rust Analyzer will send {} which will be parsed as null
7954-
;; when using plists
7955-
(when (equal 'rust-analyzer server-id)
7956-
(-> capabilities
7957-
(lsp:server-capabilities-text-document-sync?)
7958-
(lsp:set-text-document-sync-options-save? t)))
7955+
(pcase server-id
7956+
;; we know that Rust Analyzer will send {} which will be parsed as null
7957+
;; when using plists
7958+
('rust-analyzer
7959+
(-> capabilities
7960+
(lsp:server-capabilities-text-document-sync?)
7961+
(lsp:set-text-document-sync-options-save? t))))
79597962

79607963
(setf (lsp--workspace-server-capabilities workspace) capabilities
79617964
(lsp--workspace-status workspace) 'initialized)
@@ -8365,7 +8368,7 @@ nil."
83658368
(error "The package %s is not installed. Unable to find %s" package path))
83668369
path))
83678370

8368-
(cl-defun lsp--npm-dependency-install (callback error-callback &key package &allow-other-keys)
8371+
(cl-defun lsp--npm-dependency-install (callback error-callback &key package version &allow-other-keys)
83698372
(if-let* ((npm-binary (executable-find "npm")))
83708373
(progn
83718374
;; Explicitly `make-directory' to work around NPM bug in
@@ -8391,7 +8394,9 @@ nil."
83918394
"--prefix"
83928395
(f-join lsp-server-install-dir "npm" package)
83938396
"install"
8394-
package))
8397+
(if-let* ((version (lsp-resolve-value version)))
8398+
(format "%s@%s" package version)
8399+
package)))
83958400
(lsp-log "Unable to install %s via `npm' because it is not present" package)
83968401
nil))
83978402

@@ -9599,15 +9604,19 @@ This avoids overloading the server with many files when starting Emacs."
95999604
(declare-function package-desc-version "ext:package")
96009605
(declare-function package--alist "ext:package")
96019606

9607+
(defun lsp-package-version ()
9608+
"Returns a string with the version of the lsp-mode package"
9609+
(package-version-join
9610+
(package-desc-version
9611+
(car (alist-get 'lsp-mode (package--alist))))))
9612+
96029613
(defun lsp-version ()
96039614
"Return string describing current version of `lsp-mode'."
96049615
(interactive)
96059616
(unless (featurep 'package)
96069617
(require 'package))
96079618
(let ((ver (format "lsp-mode %s, Emacs %s, %s"
9608-
(package-version-join
9609-
(package-desc-version
9610-
(car (alist-get 'lsp-mode (package--alist)))))
9619+
(lsp-package-version)
96119620
emacs-version
96129621
system-type)))
96139622
(if (called-interactively-p 'interactive)

lsp-protocol.el

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,11 @@ See `-let' for a description of the destructuring mechanism."
465465
(lsp-interface (terraform-ls:Providers (:v :provider_requirements :installed_providers) nil))
466466
(lsp-interface (terraform-ls:module.terraform (:v :required_version :discovered_version)))
467467

468+
(lsp-interface
469+
(copilot-ls:SignInInitiateResponse (:status :userCode :verificationUri :expiresIn :interval :user) nil)
470+
(copilot-ls:SignInConfirmResponse (:status :user))
471+
(copilot-ls:CheckStatusResponse (:status :user)))
472+
468473

469474
;; begin autogenerated code
470475

@@ -689,7 +694,7 @@ See `-let' for a description of the destructuring mechanism."
689694
(SemanticHighlightingCapabilities nil (:semanticHighlighting))
690695
(SemanticHighlightingInformation (:line) (:tokens))
691696
(SemanticHighlightingServerCapabilities nil (:scopes))
692-
(ServerCapabilities nil (:callHierarchyProvider :codeActionProvider :codeLensProvider :colorProvider :completionProvider :declarationProvider :definitionProvider :documentFormattingProvider :documentHighlightProvider :documentLinkProvider :documentOnTypeFormattingProvider :documentRangeFormattingProvider :documentSymbolProvider :executeCommandProvider :experimental :foldingRangeProvider :hoverProvider :implementationProvider :referencesProvider :renameProvider :selectionRangeProvider :semanticHighlighting :signatureHelpProvider :textDocumentSync :typeDefinitionProvider :typeHierarchyProvider :workspace :workspaceSymbolProvider :semanticTokensProvider))
697+
(ServerCapabilities nil (:callHierarchyProvider :codeActionProvider :codeLensProvider :colorProvider :completionProvider :declarationProvider :definitionProvider :documentFormattingProvider :documentHighlightProvider :documentLinkProvider :documentOnTypeFormattingProvider :documentRangeFormattingProvider :documentSymbolProvider :executeCommandProvider :experimental :foldingRangeProvider :hoverProvider :implementationProvider :referencesProvider :renameProvider :selectionRangeProvider :semanticHighlighting :signatureHelpProvider :textDocumentSync :typeDefinitionProvider :typeHierarchyProvider :workspace :workspaceSymbolProvider :semanticTokensProvider :inlineCompletionProvider))
693698
(ServerInfo (:name) (:version))
694699
(SignatureHelp (:signatures) (:activeParameter :activeSignature))
695700
(SignatureHelpCapabilities nil (:contextSupport :dynamicRegistration :signatureInformation))

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ nav:
8484
- Fortran: page/lsp-fortran.md
8585
- Futhark: page/lsp-futhark.md
8686
- GDScript: page/lsp-gdscript.md
87+
- Github Copilot: page/lsp-copilot.md
8788
- Gleam: page/lsp-gleam.md
8889
- GLSL: page/lsp-glsl.md
8990
- GNAT Project: page/lsp-gpr.md

0 commit comments

Comments
 (0)