From a60adf3634ae5af5a33c1f2571a91f17c1a96473 Mon Sep 17 00:00:00 2001 From: Rodrigo Kassick Date: Thu, 5 Dec 2024 16:40:48 -0300 Subject: [PATCH] feat: lsp-copilot client for copilot-node-server --- clients/lsp-copilot.el | 246 +++++++++++++++++++++++++++++++++++++++++ docs/lsp-clients.json | 8 ++ lsp-mode.el | 2 +- mkdocs.yml | 1 + 4 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 clients/lsp-copilot.el diff --git a/clients/lsp-copilot.el b/clients/lsp-copilot.el new file mode 100644 index 00000000000..6452023b712 --- /dev/null +++ b/clients/lsp-copilot.el @@ -0,0 +1,246 @@ +;;; lsp-copilot.el --- lsp-mode client for copilot -*- lexical-binding: t -*- + +;; Copyright (C) 2024 Rodrigo Virote Kassick + +;; Author: Rodrigo Virote Kassick +;; 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 . + +;; Commentary: + +;; LSP client for the copilot node server -- https://www.npmjs.com/package/copilot-node-server + +;; Package-Requires: (lsp-mode secrets s compile dash cl-lib request company) + +;; 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/copilot-node-server")) + +(defcustom lsp-copilot-enabled t + "Whether the server should be started to provide completions." + :type 'boolean + :group 'lsp-copilot) + +(defcustom lsp-copilot-langserver-command-args '("--stdio") + "Command to start copilot-langserver." + :type '(repeat string) + :group 'lsp-copilot) + +(defcustom lsp-copilot-executable "copilot-lsp" + "The system-wise executable of lsp-copilot. +When this executable is not found, you can stil use +lsp-install-server to fetch an emacs-local version of the LSP." + :type 'string + :group 'lsp-copilot) + + +(defcustom lsp-copilot-major-modes '(python-mode + python-ts-mode + go-mode + go-ts-mode + js-mode + js-ts-mode + java-mode + java-ts-mode + kotlin-mode + kotlin-ts-mode + ruby-mode + ruby-ts-mode + rust-mode + rust-ts-mode + tsx-ts-mode + typescript-mode + typescript-ts-mode + vue-mode + yaml-mode + yaml-ts-mode) + + "The major modes for which lsp-copilot should be used" + :type '(repeat symbol) + :group 'lsp-copilot) + +(defcustom lsp-copilot-server-disabled-languages nil + "The lanuages 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) + +(lsp-interface + (CopilotSignInInitiateResponse (:status :userCode :verificationUri :expiresIn :interval :user) nil) + (CopilotSignInConfirmResponse (:status :user)) + (CopilotCheckStatusResponse (:status :user))) + +(lsp-dependency 'lsp-copilot + `(:system ,lsp-copilot-executable) + '(:npm :package "copilot-node-server" + :path "language-server.js")) + + +(defun lsp-copilot--client-active-for-mode-p (fname mode) + (and lsp-copilot-enabled (member mode lsp-copilot-major-modes))) + +(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)) + '(lsp-copilot lsp-copilot-remote))))) + +(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)) + '(lsp-copilot lsp-copilot-remote))) + (-if-let (checkStatusResponse (with-lsp-workspace workspace + (lsp-request "checkStatus" '(:dummy "dummy")))) + (-let* (((&CopilotCheckStatusResponse? :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 () + (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 () + (interactive) + + (-when-let (workspace (--some (lsp-find-workspace it) '(lsp-copilot lsp-copilot-remote))) + (with-lsp-workspace workspace + (-when-let* ((response (lsp-request "signInInitiate" '(:dummy "dummy")))) + (-let (((&CopilotSignInInitiateResponse? :status :user-code :verification-uri :expires-in :interval :user) response)) + + ;; Bail if already signed in + (when (s-equals-p status "AlreadySignedIn") + (lsp-message "Copilot :: Already signed in as %s" user)) + + (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-message "Verifying...") + (-let* ((confirmResponse (lsp-request "signInConfirm" (list :userCode user-code))) + ((&CopilotSignInConfirmResponse? :status :user) confirmResponse)) + (when (s-equals-p status "NotAuthorized") + (user-error "User %s is not authorized" user)) + (lsp-message "User %s is authorized: %s" user status)) + + ;; Do we need to confirm? + (-let* ((checkStatusResponse (lsp-request "checkStatus" '(:dummy "dummy"))) + ((&CopilotCheckStatusResponse? :status :user) checkStatusResponse)) + (when (s-equals-p status "NotAuthorized") + (user-error "User %s is not authorized" user)) + + (lsp-message "Authenticated as %s" user))))))) + + +(defun lsp-copilot--server-initialization-options () + ;; Trying to replicate Copilot.vim initialization here ... + (list :editorInfo (list :name "emacs" :version (symbol-value 'emacs-version)) + :editorPluginInfo (list :name "lsp-copilot" :version "1.38.0") + :editorConfig (list :enableAutoCompletions lsp-copilot-enabled + :disabledLanguages lsp-copilot-server-disabled-languages) + :name "emacs" + :version "0.1.0")) + +(defun lsp-copilot--server-initialized-fn (workspace) + (unless (lsp-copilot-authenticated-as) + (lsp-copilot-login))) + +(defun lsp-copilot--cmdline () + (-if-let (candidates (directory-files-recursively + (f-join lsp-server-install-dir "npm" "copilot-node-server") + "^language-server.js$")) + `("node" ,(car candidates) ,@lsp-copilot-langserver-command-args) + (error "language-server.js not found"))) + +;; Server installed by emacs +(lsp-register-client + (make-lsp-client + :server-id 'lsp-copilot + :new-connection (lsp-stdio-connection #'lsp-copilot--cmdline) + :activation-fn #'lsp-copilot--client-active-for-mode-p + :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 'lsp-copilot callback error-callback)) + :notification-handlers (lsp-ht + ("$/progress" (lambda (&rest args) (lsp-message "$/progress with %S" args))) + ("featureFlagsNotification" #'ignore) + ("statusNotification" #'ignore) + ("window/logMessage" #'lsp--window-log-message) + ("conversation/preconditionsNotification" #'ignore)))) + +(lsp-register-client + (make-lsp-client + :server-id 'lsp-copilot-remote + :new-connection (lsp-stdio-connection (lambda () + `(,lsp-copilot-executable ,@lsp-copilot-langserver-command-args))) + :activation-fn #'lsp-copilot--client-active-for-mode-p + :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 + :notification-handlers (lsp-ht + ("$/progress" (lambda (&rest args) (lsp-message "$/progress with %S" args))) + ("featureFlagsNotification" #'ignore) + ("statusNotification" #'ignore) + ("window/logMessage" #'lsp--window-log-message) + ("conversation/preconditionsNotification" #'ignore)))) + +(lsp-consistency-check lsp-copilot) + +(provide 'lsp-copilot) diff --git a/docs/lsp-clients.json b/docs/lsp-clients.json index 6a3eb70445a..3af660efbaf 100644 --- a/docs/lsp-clients.json +++ b/docs/lsp-clients.json @@ -116,6 +116,14 @@ "lsp-install-server": "camells", "debugger": "Not available" }, + { + "name": "copilot", + "full-name": "Github Copilot", + "server-name": "copilot-node-server", + "server-url": "https://www.npmjs.com/package/copilot-node-server", + "installation-url": "https://www.npmjs.com/package/copilot-node-server", + "debugger": "Not available" + }, { "name": "credo", "full-name": "Credo", diff --git a/lsp-mode.el b/lsp-mode.el index 3e4aa7ed94b..b9cd8bf6b06 100644 --- a/lsp-mode.el +++ b/lsp-mode.el @@ -176,7 +176,7 @@ As defined by the Language Server Protocol 3.16." '( ccls lsp-actionscript lsp-ada lsp-angular lsp-ansible lsp-asm lsp-astro lsp-autotools lsp-awk lsp-bash lsp-beancount lsp-bufls lsp-clangd lsp-clojure lsp-cmake lsp-cobol lsp-credo lsp-crystal lsp-csharp lsp-css - lsp-cucumber lsp-cypher lsp-d lsp-dart lsp-dhall lsp-docker lsp-dockerfile + lsp-copilot lsp-cucumber lsp-cypher lsp-d lsp-dart lsp-dhall lsp-docker lsp-dockerfile lsp-earthly lsp-elixir lsp-elm lsp-emmet lsp-erlang lsp-eslint lsp-fortran lsp-futhark lsp-fsharp lsp-gdscript lsp-gleam lsp-glsl lsp-go lsp-golangci-lint lsp-grammarly lsp-graphql lsp-groovy lsp-hack lsp-haskell lsp-haxe lsp-idris lsp-java diff --git a/mkdocs.yml b/mkdocs.yml index f182a9aa306..d9d89650d77 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,6 +84,7 @@ nav: - Fortran: page/lsp-fortran.md - Futhark: page/lsp-futhark.md - GDScript: page/lsp-gdscript.md + - Github Copilot: page/lsp-copilot.md - Gleam: page/lsp-gleam.md - GLSL: page/lsp-glsl.md - GNAT Project: page/lsp-gpr.md