From fea597b1b90f6dd951bbc9864a99c1510e373184 Mon Sep 17 00:00:00 2001 From: cxxxr Date: Fri, 27 Sep 2024 21:31:00 +0900 Subject: [PATCH 1/3] refactor lem-copilot --- .../copilot/{internal.lisp => client.lisp} | 190 ++++++++---------- extensions/copilot/copilot.lisp | 149 +++++++------- extensions/copilot/lem-copilot.asd | 4 +- extensions/copilot/logger.lisp | 19 ++ extensions/copilot/utils.lisp | 12 ++ 5 files changed, 196 insertions(+), 178 deletions(-) rename extensions/copilot/{internal.lisp => client.lisp} (54%) create mode 100644 extensions/copilot/logger.lisp create mode 100644 extensions/copilot/utils.lisp diff --git a/extensions/copilot/internal.lisp b/extensions/copilot/client.lisp similarity index 54% rename from extensions/copilot/internal.lisp rename to extensions/copilot/client.lisp index 672502749..7b4fa7731 100644 --- a/extensions/copilot/internal.lisp +++ b/extensions/copilot/client.lisp @@ -1,12 +1,13 @@ -(uiop:define-package :lem-copilot/internal - (:use :cl) - (:export :copilot-root - :copilot-path - :copilot-dead - :hash - :pretty-json - :run-agent +(uiop:define-package :lem-copilot/client + (:use :cl + :lem-copilot/utils + :lem-copilot/logger) + (:export :run-client + :client-process :connect + :request + :request-async + :notify :initialize :initialized :set-editor-info @@ -27,98 +28,83 @@ :text-document/inline-completion :text-document/did-show-completion :$/cancel-request)) -(in-package :lem-copilot/internal) +(in-package :lem-copilot/client) (defparameter *logging-output* t) (defparameter *logging-request* nil) +(defparameter *display-copilot-warning* t) -(defvar *logs* (lem/common/ring:make-ring 1000)) -(defvar *log-lock* (bt2:make-lock :name "copilot log lock")) - -(defgeneric copilot-root ()) -(defgeneric copilot-dead ()) - -(defun copilot-path () - (merge-pathnames "lib/node_modules/copilot-node-server/copilot/dist/language-server.js" - (copilot-root))) - -(defun hash (&rest args) - (alexandria:plist-hash-table args :test 'equal)) - -(defun pretty-json (params) - (with-output-to-string (stream) - (yason:encode params (yason:make-json-output-stream stream)))) +(defstruct client + jsonrpc + process + stream) -(defun do-log (output) - (bt2:with-lock-held (*log-lock*) - (lem/common/ring:ring-push *logs* output))) +(defun run-client (&key process) + (let ((stream (lem-lsp-mode/async-process-stream:make-input-stream + process + :logger (when *logging-output* 'logger))) + (client (jsonrpc:make-client))) + (make-client :jsonrpc client + :process process + :stream stream))) -(defun debug-log (type method params) +(defun request-log (type method params) (when *logging-request* (do-log (format nil "~A ~A ~A~%" type method (pretty-json params))))) +(defun display-copilot-warning () + (when *display-copilot-warning* + (lem:display-popup-message + (format nil + "~{~A~^~%~}" + '("Copilot has issued a warning." + "If it does not work properly, please execute `M-x copilot-restart`." + "" + "To view the copilot log, execute `M-x test/copilot-log`.")) + :style '(:gravity :top) + :timeout 10))) + (defun logger (output) (when (search "MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 change listeners added to [EventEmitter]." output) - (copilot-dead)) + (display-copilot-warning)) (do-log output)) -(defun write-log (stream) - (loop :for i :downfrom (1- (lem/common/ring:ring-length *logs*)) :to 0 - :for log := (lem/common/ring:ring-ref *logs* i) - :do (write-line log stream))) +(defun connect (client) + (jsonrpc/client:client-connect-using-class + (client-jsonrpc client) + 'lem-lsp-mode/lem-stdio-transport:lem-stdio-transport + :process (client-process client) + :stream (client-stream client))) -(defstruct agent - client - process - stream) +(defun request (client method params) + (request-log :request method params) + (jsonrpc:call (client-jsonrpc client) method params)) -(defun run-agent () - (let* ((process - (async-process:create-process - (list "node" (namestring (copilot-path)) "--stdio"))) - (stream (lem-lsp-mode/async-process-stream:make-input-stream - process - :logger (when *logging-output* 'logger))) - (client (jsonrpc:make-client))) - (make-agent :client client - :process process - :stream stream))) - -(defun connect (agent) - (jsonrpc/client:client-connect-using-class (agent-client agent) - 'lem-lsp-mode/lem-stdio-transport:lem-stdio-transport - :process (agent-process agent) - :stream (agent-stream agent))) - -(defun request (agent method params) - (debug-log :request method params) - (jsonrpc:call (agent-client agent) method params)) - -(defun request-async (agent method params &key callback error-callback) - (debug-log :request-async method params) - (jsonrpc:call-async (agent-client agent) +(defun request-async (client method params &key callback error-callback) + (request-log :request-async method params) + (jsonrpc:call-async (client-jsonrpc client) method params callback error-callback)) -(defun notify (agent method params) - (debug-log :notify method params) - (jsonrpc:notify (agent-client agent) method params)) +(defun notify (client method params) + (request-log :notify method params) + (jsonrpc:notify (client-jsonrpc client) method params)) -(defun initialize (agent) - (request agent +(defun initialize (client) + (request client "initialize" (hash "capabilities" (hash "workspace" (hash "workspaceFolders" t "editorConfiguration" (hash "enableAutoCompletions" t)))))) -(defun initialized (agent) - (notify agent "initialized" (hash))) +(defun initialized (client) + (notify client "initialized" (hash))) -(defun set-editor-info (agent) - (request agent +(defun set-editor-info (client) + (request client "setEditorInfo" (hash "editorInfo" (hash "name" "Lem" @@ -126,72 +112,72 @@ "editorPluginInfo" (hash "name" "lem-copilot" "version" "0.0")))) -(defun sign-in-initiate (agent) - (request agent +(defun sign-in-initiate (client) + (request client "signInInitiate" (hash))) -(defun sign-in-confirm (agent user-code &key callback) - (request-async agent +(defun sign-in-confirm (client user-code &key callback) + (request-async client "signInConfirm" (hash "userCode" user-code) :callback callback :error-callback #'default-error-callback)) -(defun check-status (agent) - (request agent +(defun check-status (client) + (request client "checkStatus" (hash))) -(defun text-document/did-open (agent &key uri language-id version text) - (notify agent +(defun text-document/did-open (client &key uri language-id version text) + (notify client "textDocument/didOpen" (hash "textDocument" (hash "uri" uri "languageId" language-id "version" version "text" text)))) -(defun text-document/did-close (agent &key uri) - (notify agent +(defun text-document/did-close (client &key uri) + (notify client "textDocument/didClose" (hash "textDocument" (hash "uri" uri)))) -(defun text-document/did-change (agent &key uri version content-changes) - (notify agent +(defun text-document/did-change (client &key uri version content-changes) + (notify client "textDocument/didChange" (hash "textDocument" (hash "uri" uri "version" version) "contentChanges" content-changes))) -(defun text-document/did-focus (agent &key uri) - (notify agent +(defun text-document/did-focus (client &key uri) + (notify client "textDocument/didFocus" (hash "textDocument" (hash "uri" uri)))) -(defun get-completions (agent &key doc callback) - (request-async agent +(defun get-completions (client &key doc callback) + (request-async client "getCompletions" (hash "doc" doc) :callback callback :error-callback #'default-error-callback)) -(defun notify-shown (agent uuid) - (request-async agent "notifyShown" (hash "uuid" uuid))) +(defun notify-shown (client uuid) + (request-async client "notifyShown" (hash "uuid" uuid))) -(defun notify-accepted (agent uuid) - (request-async agent +(defun notify-accepted (client uuid) + (request-async client "notifyAccepted" (hash "uuid" uuid) :error-callback #'default-error-callback)) -(defun notify-rejected (agent uuid) - (request-async agent +(defun notify-rejected (client uuid) + (request-async client "notifyRejected" (hash "uuids" (vector uuid)) :error-callback #'default-error-callback)) -(defun get-completions-cycling (agent &key doc callback error-callback) - (request-async agent +(defun get-completions-cycling (client &key doc callback error-callback) + (request-async client "getCompletionsCycling" (hash "doc" doc) :callback callback @@ -201,14 +187,14 @@ (defparameter +trigger-kind.trigger-character+ 1) (defun text-document/inline-completion - (agent &key callback + (client &key callback error-callback uri position insert-spaces tab-size (trigger-kind +trigger-kind.trigger-character+)) - (request-async agent + (request-async client "textDocument/inlineCompletion" (hash "textDocument" (hash "uri" uri) "position" position @@ -219,13 +205,13 @@ :error-callback (when error-callback (lambda (&rest args) (apply error-callback args))))) -(defun text-document/did-show-completion (agent item) - (notify agent +(defun text-document/did-show-completion (client item) + (notify client "textDocument/didShowCompletion" (hash "item" item))) -(defun $/cancel-request (agent id) - (notify agent +(defun $/cancel-request (client id) + (notify client "$/cancelRequest" (hash "id" id))) diff --git a/extensions/copilot/copilot.lisp b/extensions/copilot/copilot.lisp index 5d7656d1b..3a7cb0a9d 100644 --- a/extensions/copilot/copilot.lisp +++ b/extensions/copilot/copilot.lisp @@ -1,6 +1,9 @@ (defpackage :lem-copilot - (:use :cl :lem) - (:local-nicknames (:copilot :lem-copilot/internal))) + (:use :cl + :lem + :lem-copilot/utils) + (:local-nicknames (:client :lem-copilot/client) + (:logger :lem-copilot/logger))) (in-package :lem-copilot) (define-attribute suggestion-attribute @@ -11,26 +14,39 @@ (define-condition already-sign-in (editor-error) ()) -(defmethod copilot:copilot-root () +(defvar *client* nil) + +(defun client () + *client*) + +(defun copilot-root () (merge-pathnames "copilot/" (lem-home))) -(defvar *agent* nil) +(defun copilot-path () + (merge-pathnames "lib/node_modules/copilot-node-server/copilot/dist/language-server.js" + (copilot-root))) + +(defun installed-copilot-server-p () + (uiop:file-exists-p (copilot-path))) + +(defun run-process () + (async-process:create-process (list "node" + (namestring (copilot-path)) + "--stdio"))) + +(defun kill-process () + (when (client) + (async-process:delete-process (client:client-process (client))))) -(defun setup-agent () - (let ((agent (copilot:run-agent))) - (add-hook *exit-editor-hook* - (lambda () - (async-process:delete-process (copilot::agent-process agent)))) - (copilot:connect agent) - (copilot:initialize agent) - (copilot:initialized agent) - (copilot:set-editor-info agent) - agent)) +(add-hook *exit-editor-hook* 'kill-process) -(defun agent () - (unless *agent* - (setf *agent* (setup-agent))) - *agent*) +(defun setup-client () + (let ((client (client:run-client :process (run-process)))) + (client:connect client) + (client:initialize client) + (client:initialized client) + (client:set-editor-info client) + (setf *client* client))) (defun enable-copilot-p () (config :copilot)) @@ -44,7 +60,7 @@ (command (list "npm" "-g" "--prefix" - (namestring (copilot:copilot-root)) + (namestring (copilot-root)) "install" "copilot-node-server@1.40.0"))) (erase-buffer buffer) @@ -57,33 +73,6 @@ :output output :error-output output))))) -(defun installed-copilot-server-p () - (uiop:file-exists-p (copilot:copilot-path))) - -(defun reset-buffers () - (dolist (buffer (remove-if-not #'copilot-mode-p (buffer-list))) - (setf (buffer-version buffer) 0) - (notify-text-document/did-open buffer))) - -(define-command copilot-restart () () - (async-process:delete-process (lem-copilot/internal::agent-process lem-copilot::*agent*)) - (setf *agent* nil) - (handler-case (copilot-login) (already-sign-in ())) - (reset-buffers) - (message "copilot restarted")) - -(defmethod copilot:copilot-dead () - (display-popup-message (format nil - "~{~A~^~%~}" - '("Copilot has issued a warning. " - "If it does not work properly, please execute `M-x copilot-restart`." - "" - "To view the copilot log, execute `M-x test/copilot-log`.")) - :style '(:gravity :top) - :timeout 10) - #+(or) - (copilot-restart)) - ;;; login (defun make-verification-buffer (user-code verification-uri) @@ -121,9 +110,8 @@ (define-command copilot-login () () (unless (installed-copilot-server-p) (copilot-install-server)) - (setf *agent* nil) - (let* ((agent (agent)) - (response (copilot:sign-in-initiate agent)) + (let* ((client (setup-client)) + (response (client:sign-in-initiate client)) (status (gethash "status" response)) (user-code (gethash "userCode" response)) (verification-uri (gethash "verificationUri" response)) @@ -135,8 +123,8 @@ (open-external-file verification-uri) (redraw-display) (let ((finished nil)) - (copilot:sign-in-confirm - agent + (client:sign-in-confirm + client user-code :callback (lambda (response) (send-event (lambda () @@ -196,8 +184,8 @@ (setf (buffer-value buffer 'showing-suggestions-p) showing-suggestions-p)) (defun point-to-lsp-position (point) - (copilot:hash "line" (1- (line-number-at-point point)) - "character" (point-charpos point))) + (hash "line" (1- (line-number-at-point point)) + "character" (point-charpos point))) (defun move-to-lsp-position (point position) (move-to-line point (1+ (gethash "line" position))) @@ -210,19 +198,19 @@ :text (buffer-text buffer))) (defun notify-text-document/did-open (buffer) - (apply #'copilot:text-document/did-open - (agent) + (apply #'client:text-document/did-open + (client) (text-document-params buffer))) (defun notify-text-document/did-close (buffer) - (copilot:text-document/did-close (agent) :uri (buffer-uri buffer))) + (client:text-document/did-close (client) :uri (buffer-uri buffer))) (defun notify-text-document/did-focus (buffer) - (copilot:text-document/did-focus (agent) :uri (buffer-uri buffer))) + (client:text-document/did-focus (client) :uri (buffer-uri buffer))) (defun notify-text-document/did-change (buffer content-changes) (let ((version (incf (buffer-version buffer)))) - (copilot:text-document/did-change (agent) + (client:text-document/did-change (client) :uri (buffer-uri buffer) :version version :content-changes content-changes))) @@ -242,7 +230,7 @@ (unless (installed-copilot-server-p) (copilot-install-server) (reset-buffers)) - (unless *agent* + (unless (client) (handler-case (copilot-login) (already-sign-in ()))) (add-hook (variable-value 'kill-buffer-hook :buffer (current-buffer)) 'on-kill-buffer) (add-hook (variable-value 'before-change-functions :buffer (current-buffer)) 'on-before-change) @@ -271,15 +259,15 @@ (etypecase arg (string (let ((position (point-to-lsp-position point))) - (copilot:hash "range" (copilot:hash "start" position - "end" position) - "text" arg))) + (hash "range" (hash "start" position + "end" position) + "text" arg))) (integer (with-point ((end point)) (character-offset end arg) - (copilot:hash "range" (copilot:hash "start" (point-to-lsp-position point) - "end" (point-to-lsp-position end)) - "text" ""))))) + (hash "range" (hash "start" (point-to-lsp-position point) + "end" (point-to-lsp-position end)) + "text" ""))))) (defvar *inhibit-did-change-notification* nil) @@ -302,6 +290,17 @@ (when (copilot-mode-p buffer) (notify-text-document/did-focus buffer)))) +(defun reset-buffers () + (dolist (buffer (remove-if-not #'copilot-mode-p (buffer-list))) + (setf (buffer-version buffer) 0) + (notify-text-document/did-open buffer))) + +(define-command copilot-restart () () + (async-process:delete-process (client:client-process (client))) + (handler-case (copilot-login) (already-sign-in ())) + (reset-buffers) + (message "copilot restarted")) + (defvar *delay-complete* 100) (defvar *complete-timer* nil) @@ -414,14 +413,14 @@ (copilot-next-suggestion (read-key) (inline-completion point - :trigger-kind copilot:+trigger-kind.invoked+ + :trigger-kind client:+trigger-kind.invoked+ :index (mod (1+ index) (length items)) :cycling t :show-loading-spinner t)) (copilot-previous-suggestion (read-key) (inline-completion point - :trigger-kind copilot:+trigger-kind.invoked+ + :trigger-kind client:+trigger-kind.invoked+ :index (mod (1- index) (length items)) :cycling t :show-loading-spinner t)) @@ -440,8 +439,8 @@ (spinner (when show-loading-spinner (lem/loading-spinner:start-loading-spinner :line :point point))) (request - (copilot:text-document/inline-completion - (agent) + (client:text-document/inline-completion + (client) :callback (lambda (response) (send-event (lambda () (when spinner @@ -471,7 +470,7 @@ (defun cancel-inline-completion () (unshow-inline-completion (current-point)) (when *inline-completion-request* - (copilot:$/cancel-request (agent) (jsonrpc:request-id *inline-completion-request*)) + (client:$/cancel-request (client) (jsonrpc:request-id *inline-completion-request*)) (setf *inline-completion-request* nil *completion-canceled* t))) @@ -527,10 +526,10 @@ ;;; test (define-command test/copilot-document () () - (let ((response (copilot::request (agent) - "testing/getDocument" - (copilot:hash "uri" (buffer-uri (current-buffer)))))) - (show-message (copilot:pretty-json response)) + (let ((response (client:request (client) + "testing/getDocument" + (hash "uri" (buffer-uri (current-buffer)))))) + (show-message (pretty-json response)) (assert (equal (gethash "text" response) (buffer-text (current-buffer)))))) @@ -538,5 +537,5 @@ (let* ((buffer (make-buffer "*copilot-log*" :enable-undo-p nil))) (erase-buffer buffer) (with-open-stream (stream (make-buffer-output-stream (buffer-point buffer))) - (copilot::write-log stream)) + (logger:write-log stream)) (pop-to-buffer buffer))) diff --git a/extensions/copilot/lem-copilot.asd b/extensions/copilot/lem-copilot.asd index 123114a58..d11345781 100644 --- a/extensions/copilot/lem-copilot.asd +++ b/extensions/copilot/lem-copilot.asd @@ -1,4 +1,6 @@ (defsystem "lem-copilot" :depends-on ("lem" "lem-lsp-mode") - :components ((:file "internal") + :components ((:file "utils") + (:file "logger") + (:file "client") (:file "copilot"))) diff --git a/extensions/copilot/logger.lisp b/extensions/copilot/logger.lisp new file mode 100644 index 000000000..7ff33d59d --- /dev/null +++ b/extensions/copilot/logger.lisp @@ -0,0 +1,19 @@ +(defpackage :lem-copilot/logger + (:use :cl) + (:local-nicknames (:ring :lem/common/ring)) + (:export :do-log + :write-log)) +(in-package :lem-copilot/logger) + +(defvar *logs* (ring:make-ring 1000)) +(defvar *log-lock* (bt2:make-lock :name "copilot log lock")) + +(defun do-log (output) + (bt2:with-lock-held (*log-lock*) + (ring:ring-push *logs* output))) + +(defun write-log (stream) + (bt2:with-lock-held (*log-lock*) + (loop :for i :downfrom (1- (ring:ring-length *logs*)) :to 0 + :for log := (ring:ring-ref *logs* i) + :do (write-line log stream)))) diff --git a/extensions/copilot/utils.lisp b/extensions/copilot/utils.lisp new file mode 100644 index 000000000..d7ea87743 --- /dev/null +++ b/extensions/copilot/utils.lisp @@ -0,0 +1,12 @@ +(defpackage :lem-copilot/utils + (:use :cl) + (:export :hash + :pretty-json)) +(in-package :lem-copilot/utils) + +(defun hash (&rest args) + (alexandria:plist-hash-table args :test 'equal)) + +(defun pretty-json (params) + (with-output-to-string (stream) + (yason:encode params (yason:make-json-output-stream stream)))) From a287830988e9d0ef1335fa7b6a4b9b1a90d9b6dd Mon Sep 17 00:00:00 2001 From: cxxxr Date: Sat, 28 Sep 2024 03:26:00 +0900 Subject: [PATCH 2/3] Initialize copilot asynchronously --- extensions/copilot/client.lisp | 32 +++++---- extensions/copilot/copilot.lisp | 124 +++++++++++++++++++------------- 2 files changed, 91 insertions(+), 65 deletions(-) diff --git a/extensions/copilot/client.lisp b/extensions/copilot/client.lisp index 7b4fa7731..8b4073d38 100644 --- a/extensions/copilot/client.lisp +++ b/extensions/copilot/client.lisp @@ -92,25 +92,27 @@ (request-log :notify method params) (jsonrpc:notify (client-jsonrpc client) method params)) -(defun initialize (client) - (request client - "initialize" - (hash "capabilities" - (hash "workspace" - (hash "workspaceFolders" t - "editorConfiguration" (hash "enableAutoCompletions" t)))))) +(defun initialize (client &key callback) + (request-async client + "initialize" + (hash "capabilities" + (hash "workspace" + (hash "workspaceFolders" t + "editorConfiguration" (hash "enableAutoCompletions" t)))) + :callback callback)) (defun initialized (client) (notify client "initialized" (hash))) -(defun set-editor-info (client) - (request client - "setEditorInfo" - (hash "editorInfo" - (hash "name" "Lem" - "version" (lem:get-version-string)) - "editorPluginInfo" (hash "name" "lem-copilot" - "version" "0.0")))) +(defun set-editor-info (client &key callback) + (request-async client + "setEditorInfo" + (hash "editorInfo" + (hash "name" "Lem" + "version" (lem:get-version-string)) + "editorPluginInfo" (hash "name" "lem-copilot" + "version" "0.0")) + :callback callback)) (defun sign-in-initiate (client) (request client diff --git a/extensions/copilot/copilot.lisp b/extensions/copilot/copilot.lisp index 3a7cb0a9d..aceb9f848 100644 --- a/extensions/copilot/copilot.lisp +++ b/extensions/copilot/copilot.lisp @@ -40,20 +40,38 @@ (add-hook *exit-editor-hook* 'kill-process) -(defun setup-client () - (let ((client (client:run-client :process (run-process)))) - (client:connect client) - (client:initialize client) - (client:initialized client) - (client:set-editor-info client) - (setf *client* client))) - (defun enable-copilot-p () (config :copilot)) (defun enable-copilot () (setf (config :copilot) t)) + +(defun initialize (client then) + (client:initialize client + :callback (lambda (response) + (send-event (lambda () + (funcall then response)))))) + +(defun set-editor-info (client then) + (client:set-editor-info client + :callback (lambda (response) + (send-event (lambda () + (funcall then response)))))) + +(defun setup-client-async (then) + (let ((client (client:run-client :process (run-process)))) + (client:connect client) + (initialize client + (lambda (response) + (declare (ignore response)) + (client:initialized client) + (set-editor-info client + (lambda (response) + (declare (ignore response)) + (setf *client* client) + (funcall then))))))) + (define-command copilot-install-server () () (let* ((buffer (make-buffer "*copilot-install-server*")) @@ -110,37 +128,38 @@ (define-command copilot-login () () (unless (installed-copilot-server-p) (copilot-install-server)) - (let* ((client (setup-client)) - (response (client:sign-in-initiate client)) - (status (gethash "status" response)) - (user-code (gethash "userCode" response)) - (verification-uri (gethash "verificationUri" response)) - (user (gethash "user" response))) - (when (equal status "AlreadySignedIn") - (error 'already-sign-in :message (format nil "Already sign in as ~A" user))) - (copy-to-clipboard user-code) - (start-login user-code verification-uri) - (open-external-file verification-uri) - (redraw-display) - (let ((finished nil)) - (client:sign-in-confirm - client - user-code - :callback (lambda (response) - (send-event (lambda () - (assert (equal "OK" (gethash "status" response))) - (show-message (format nil "Authenticated as ~A" (gethash "user" response)) - :style '(:gravity :center)) - (delete-login-message) - (setf finished t) - (redraw-display))))) - (handler-bind ((editor-abort (lambda (c) - (declare (ignore c)) - (delete-login-message)))) - - (loop :until finished - :do (sit-for 1))) - (enable-copilot)))) + (setup-client-async + (lambda () + (let* ((response (client:sign-in-initiate *client*)) + (status (gethash "status" response)) + (user-code (gethash "userCode" response)) + (verification-uri (gethash "verificationUri" response)) + (user (gethash "user" response))) + (when (equal status "AlreadySignedIn") + (error 'already-sign-in :message (format nil "Already sign in as ~A" user))) + (copy-to-clipboard user-code) + (start-login user-code verification-uri) + (open-external-file verification-uri) + (redraw-display) + (let ((finished nil)) + (client:sign-in-confirm + (client) + user-code + :callback (lambda (response) + (send-event (lambda () + (assert (equal "OK" (gethash "status" response))) + (show-message (format nil "Authenticated as ~A" (gethash "user" response)) + :style '(:gravity :center)) + (delete-login-message) + (setf finished t) + (redraw-display))))) + (handler-bind ((editor-abort (lambda (c) + (declare (ignore c)) + (delete-login-message)))) + + (loop :until finished + :do (sit-for 1))) + (enable-copilot)))))) ;;; utils @@ -230,14 +249,16 @@ (unless (installed-copilot-server-p) (copilot-install-server) (reset-buffers)) - (unless (client) - (handler-case (copilot-login) (already-sign-in ()))) - (add-hook (variable-value 'kill-buffer-hook :buffer (current-buffer)) 'on-kill-buffer) - (add-hook (variable-value 'before-change-functions :buffer (current-buffer)) 'on-before-change) - (add-hook *window-show-buffer-functions* 'on-window-show-buffer) - (add-hook *switch-to-window-hook* 'on-switch-to-window) - (add-hook *post-command-hook* 'on-post-command) - (notify-text-document/did-open (current-buffer))) + (flet ((fn () + (add-hook (variable-value 'kill-buffer-hook :buffer (current-buffer)) 'on-kill-buffer) + (add-hook (variable-value 'before-change-functions :buffer (current-buffer)) 'on-before-change) + (add-hook *window-show-buffer-functions* 'on-window-show-buffer) + (add-hook *switch-to-window-hook* 'on-switch-to-window) + (add-hook *post-command-hook* 'on-post-command) + (notify-text-document/did-open (current-buffer)))) + (if (client) + (fn) + (setup-client-async #'fn)))) (defun copilot-mode-off () (remove-hook (variable-value 'kill-buffer-hook :buffer (current-buffer)) 'on-kill-buffer) @@ -297,9 +318,12 @@ (define-command copilot-restart () () (async-process:delete-process (client:client-process (client))) - (handler-case (copilot-login) (already-sign-in ())) - (reset-buffers) - (message "copilot restarted")) + (setup-client-async (lambda () + (reset-buffers) + (show-message "copilot restarted" + :style '(:gravity :center) + :timeout 3) + (redraw-display)))) (defvar *delay-complete* 100) (defvar *complete-timer* nil) From 71133d62d69b4b8f56d34b17dcbcdf4bf8d69a12 Mon Sep 17 00:00:00 2001 From: cxxxr Date: Sat, 28 Sep 2024 03:32:45 +0900 Subject: [PATCH 3/3] refactor lem-copilot --- extensions/copilot/copilot.lisp | 213 +++++--------------------- extensions/copilot/install.lisp | 25 +++ extensions/copilot/languages.lisp | 37 +++++ extensions/copilot/lem-copilot.asd | 6 +- extensions/copilot/login.lisp | 77 ++++++++++ extensions/copilot/test-commands.lisp | 21 +++ 6 files changed, 200 insertions(+), 179 deletions(-) create mode 100644 extensions/copilot/install.lisp create mode 100644 extensions/copilot/languages.lisp create mode 100644 extensions/copilot/login.lisp create mode 100644 extensions/copilot/test-commands.lisp diff --git a/extensions/copilot/copilot.lisp b/extensions/copilot/copilot.lisp index aceb9f848..03e6dfd3d 100644 --- a/extensions/copilot/copilot.lisp +++ b/extensions/copilot/copilot.lisp @@ -12,8 +12,6 @@ (define-attribute cycling-attribute (t :foreground "green")) -(define-condition already-sign-in (editor-error) ()) - (defvar *client* nil) (defun client () @@ -46,121 +44,6 @@ (defun enable-copilot () (setf (config :copilot) t)) - -(defun initialize (client then) - (client:initialize client - :callback (lambda (response) - (send-event (lambda () - (funcall then response)))))) - -(defun set-editor-info (client then) - (client:set-editor-info client - :callback (lambda (response) - (send-event (lambda () - (funcall then response)))))) - -(defun setup-client-async (then) - (let ((client (client:run-client :process (run-process)))) - (client:connect client) - (initialize client - (lambda (response) - (declare (ignore response)) - (client:initialized client) - (set-editor-info client - (lambda (response) - (declare (ignore response)) - (setf *client* client) - (funcall then))))))) - - -(define-command copilot-install-server () () - (let* ((buffer (make-buffer "*copilot-install-server*")) - (command (list "npm" - "-g" - "--prefix" - (namestring (copilot-root)) - "install" - "copilot-node-server@1.40.0"))) - (erase-buffer buffer) - (pop-to-buffer buffer) - (with-point ((point (buffer-point buffer) :left-inserting)) - (with-open-stream (output (make-editor-output-stream point)) - (format output "~{~A ~}~%" command) - (redraw-display) - (uiop:run-program command - :output output - :error-output output))))) - - -;;; login -(defun make-verification-buffer (user-code verification-uri) - (let* ((buffer (make-buffer "*GitHub Copilot Verification*" :temporary t)) - (point (buffer-point buffer))) - (setf (variable-value 'line-wrap :buffer buffer) nil) - (erase-buffer buffer) - (insert-string point - (format nil - "Code: ~A (Copied to clipboard) ~%please paste it into your browser.~%~A~2%" - user-code - verification-uri)) - (insert-string point "Authenticate... (Close this window with Escape or C-g.)") - (buffer-start point) - buffer)) - -(defvar *login-message* nil) - -(defun start-login (user-code verification-uri) - (setf *login-message* (display-popup-message - (make-verification-buffer user-code verification-uri) - :style '(:gravity :center) - :timeout nil)) - (add-hook *editor-abort-hook* 'abort-login)) - -(defun abort-login () - (delete-login-message) - (remove-hook *editor-abort-hook* 'abort-login)) - -(defun delete-login-message () - (when *login-message* - (delete-popup-message *login-message*) - (setf *login-message* nil))) - -(define-command copilot-login () () - (unless (installed-copilot-server-p) - (copilot-install-server)) - (setup-client-async - (lambda () - (let* ((response (client:sign-in-initiate *client*)) - (status (gethash "status" response)) - (user-code (gethash "userCode" response)) - (verification-uri (gethash "verificationUri" response)) - (user (gethash "user" response))) - (when (equal status "AlreadySignedIn") - (error 'already-sign-in :message (format nil "Already sign in as ~A" user))) - (copy-to-clipboard user-code) - (start-login user-code verification-uri) - (open-external-file verification-uri) - (redraw-display) - (let ((finished nil)) - (client:sign-in-confirm - (client) - user-code - :callback (lambda (response) - (send-event (lambda () - (assert (equal "OK" (gethash "status" response))) - (show-message (format nil "Authenticated as ~A" (gethash "user" response)) - :style '(:gravity :center)) - (delete-login-message) - (setf finished t) - (redraw-display))))) - (handler-bind ((editor-abort (lambda (c) - (declare (ignore c)) - (delete-login-message)))) - - (loop :until finished - :do (sit-for 1))) - (enable-copilot)))))) - ;;; utils (defvar *language-id-map* (make-hash-table :test 'eq)) @@ -234,6 +117,18 @@ :version version :content-changes content-changes))) +(defun initialize (client then) + (client:initialize client + :callback (lambda (response) + (send-event (lambda () + (funcall then response)))))) + +(defun set-editor-info (client then) + (client:set-editor-info client + :callback (lambda (response) + (send-event (lambda () + (funcall then response)))))) + ;;; copilot-mode (define-minor-mode copilot-mode @@ -245,6 +140,19 @@ (define-key *copilot-mode-keymap* "M-n" 'copilot-next-suggestion) (define-key *copilot-mode-keymap* "M-p" 'copilot-previous-suggestion) +(defun setup-client-async (then) + (let ((client (client:run-client :process (run-process)))) + (client:connect client) + (initialize client + (lambda (response) + (declare (ignore response)) + (client:initialized client) + (set-editor-info client + (lambda (response) + (declare (ignore response)) + (setf *client* client) + (funcall then))))))) + (defun copilot-mode-on () (unless (installed-copilot-server-p) (copilot-install-server) @@ -325,18 +233,6 @@ :timeout 3) (redraw-display)))) -(defvar *delay-complete* 100) -(defvar *complete-timer* nil) - -(defmethod execute :after ((mode copilot-mode) (command self-insert) argument) - (cond (*delay-complete* - (if *complete-timer* - (stop-timer *complete-timer*) - (setf *complete-timer* (make-idle-timer 'copilot-complete :name "Copilot Complete"))) - (start-timer *complete-timer* *delay-complete* :repeat nil)) - (t - (copilot-complete)))) - ;;; complete (defvar *inline-completion-request* nil) @@ -513,53 +409,14 @@ ;; dummy command ) - -;;; -(defun enable-copilot-mode () - (when (and (enable-copilot-p) - (not (buffer-temporary-p (current-buffer)))) - (copilot-mode t))) - -(defmacro define-language (mode (&key (language-id (alexandria:required-argument :language-id)))) - `(progn - (add-hook ,(mode-hook-variable mode) 'enable-copilot-mode) - (register-language-id ',mode ,language-id))) - -(define-language lem-js-mode:js-mode (:language-id "javascript")) -(define-language lem-rust-mode:rust-mode (:language-id "rust")) -(define-language lem-go-mode:go-mode (:language-id "go")) -(define-language lem-lisp-mode:lisp-mode (:language-id "lisp")) -(define-language lem-markdown-mode:markdown-mode (:language-id "markdown")) -(define-language lem-c-mode:c-mode (:language-id "c")) -(define-language lem-css-mode:css-mode (:language-id "css")) -(define-language lem-dart-mode:dart-mode (:language-id "dart")) -(define-language lem-json-mode:json-mode (:language-id "json")) -(define-language lem-lua-mode:lua-mode (:language-id "lua")) -(define-language lem-nim-mode:nim-mode (:language-id "nim")) -(define-language lem-ocaml-mode:ocaml-mode (:language-id "ocaml")) -(define-language lem-python-mode:python-mode (:language-id "python")) -(define-language lem-scala-mode:scala-mode (:language-id "scala")) -(define-language lem-scheme-mode:scheme-mode (:language-id "scheme")) -(define-language lem-sql-mode:sql-mode (:language-id "sql")) -(define-language lem-terraform-mode:terraform-mode (:language-id "hcl")) -(define-language lem-typescript-mode:typescript-mode (:language-id "typescript")) -(define-language lem-xml-mode:xml-mode (:language-id "xml")) -(define-language lem-yaml-mode:yaml-mode (:language-id "yaml")) -(define-language lem-swift-mode:swift-mode (:language-id "swift")) +(defparameter *delay-complete* 1) +(defvar *complete-timer* nil) - -;;; test -(define-command test/copilot-document () () - (let ((response (client:request (client) - "testing/getDocument" - (hash "uri" (buffer-uri (current-buffer)))))) - (show-message (pretty-json response)) - (assert (equal (gethash "text" response) - (buffer-text (current-buffer)))))) - -(define-command test/copilot-log () () - (let* ((buffer (make-buffer "*copilot-log*" :enable-undo-p nil))) - (erase-buffer buffer) - (with-open-stream (stream (make-buffer-output-stream (buffer-point buffer))) - (logger:write-log stream)) - (pop-to-buffer buffer))) +(defmethod execute :after ((mode copilot-mode) (command self-insert) argument) + (cond (*delay-complete* + (if *complete-timer* + (stop-timer *complete-timer*) + (setf *complete-timer* (make-idle-timer 'copilot-complete :name "Copilot Complete"))) + (start-timer *complete-timer* *delay-complete* :repeat nil)) + (t + (copilot-complete)))) diff --git a/extensions/copilot/install.lisp b/extensions/copilot/install.lisp new file mode 100644 index 000000000..69a5fab87 --- /dev/null +++ b/extensions/copilot/install.lisp @@ -0,0 +1,25 @@ +(uiop:define-package :lem-copilot/install + (:use :cl :lem) + (:local-nicknames (:client :lem-copilot/client) + (:logger :lem-copilot/logger) + (:copilot :lem-copilot)) + (:export :copilot-install-server)) +(in-package :lem-copilot/install) + +(define-command copilot-install-server () () + (let* ((buffer (make-buffer "*copilot-install-server*")) + (command (list "npm" + "-g" + "--prefix" + (namestring (copilot::copilot-root)) + "install" + "copilot-node-server@1.40.0"))) + (erase-buffer buffer) + (pop-to-buffer buffer) + (with-point ((point (buffer-point buffer) :left-inserting)) + (with-open-stream (output (make-editor-output-stream point)) + (format output "~{~A ~}~%" command) + (redraw-display) + (uiop:run-program command + :output output + :error-output output))))) diff --git a/extensions/copilot/languages.lisp b/extensions/copilot/languages.lisp new file mode 100644 index 000000000..cb78df8f4 --- /dev/null +++ b/extensions/copilot/languages.lisp @@ -0,0 +1,37 @@ +(defpackage :lem-copilot/languages + (:use :cl :lem) + (:local-nicknames (:copilot :lem-copilot)) + (:export :define-language)) +(in-package :lem-copilot/languages) + +(defun enable-copilot-mode () + (when (and (copilot::enable-copilot-p) + (not (buffer-temporary-p (current-buffer)))) + (copilot::copilot-mode t))) + +(defmacro define-language (mode (&key (language-id (alexandria:required-argument :language-id)))) + `(progn + (add-hook ,(mode-hook-variable mode) 'enable-copilot-mode) + (copilot::register-language-id ',mode ,language-id))) + +(define-language lem-js-mode:js-mode (:language-id "javascript")) +(define-language lem-rust-mode:rust-mode (:language-id "rust")) +(define-language lem-go-mode:go-mode (:language-id "go")) +(define-language lem-lisp-mode:lisp-mode (:language-id "lisp")) +(define-language lem-markdown-mode:markdown-mode (:language-id "markdown")) +(define-language lem-c-mode:c-mode (:language-id "c")) +(define-language lem-css-mode:css-mode (:language-id "css")) +(define-language lem-dart-mode:dart-mode (:language-id "dart")) +(define-language lem-json-mode:json-mode (:language-id "json")) +(define-language lem-lua-mode:lua-mode (:language-id "lua")) +(define-language lem-nim-mode:nim-mode (:language-id "nim")) +(define-language lem-ocaml-mode:ocaml-mode (:language-id "ocaml")) +(define-language lem-python-mode:python-mode (:language-id "python")) +(define-language lem-scala-mode:scala-mode (:language-id "scala")) +(define-language lem-scheme-mode:scheme-mode (:language-id "scheme")) +(define-language lem-sql-mode:sql-mode (:language-id "sql")) +(define-language lem-terraform-mode:terraform-mode (:language-id "hcl")) +(define-language lem-typescript-mode:typescript-mode (:language-id "typescript")) +(define-language lem-xml-mode:xml-mode (:language-id "xml")) +(define-language lem-yaml-mode:yaml-mode (:language-id "yaml")) +(define-language lem-swift-mode:swift-mode (:language-id "swift")) diff --git a/extensions/copilot/lem-copilot.asd b/extensions/copilot/lem-copilot.asd index d11345781..2650d0b14 100644 --- a/extensions/copilot/lem-copilot.asd +++ b/extensions/copilot/lem-copilot.asd @@ -3,4 +3,8 @@ :components ((:file "utils") (:file "logger") (:file "client") - (:file "copilot"))) + (:file "copilot") + (:file "install") + (:file "login") + (:file "languages") + (:file "test-commands"))) diff --git a/extensions/copilot/login.lisp b/extensions/copilot/login.lisp new file mode 100644 index 000000000..90570b419 --- /dev/null +++ b/extensions/copilot/login.lisp @@ -0,0 +1,77 @@ +(uiop:define-package :lem-copilot/login + (:use :cl :lem) + (:local-nicknames (:lem-copilot :lem-copilot) + (:client :lem-copilot/client) + (:copilot :lem-copilot)) + (:export :copilot-login)) +(in-package :lem-copilot/login) + +(define-condition already-sign-in (editor-error) ()) + +(defvar *login-message* nil) + +(defun make-verification-buffer (user-code verification-uri) + (let* ((buffer (make-buffer "*GitHub Copilot Verification*" :temporary t)) + (point (buffer-point buffer))) + (setf (variable-value 'line-wrap :buffer buffer) nil) + (erase-buffer buffer) + (insert-string point + (format nil + "Code: ~A (Copied to clipboard) ~%please paste it into your browser.~%~A~2%" + user-code + verification-uri)) + (insert-string point "Authenticate... (Close this window with Escape or C-g.)") + (buffer-start point) + buffer)) + +(defun start-login (user-code verification-uri) + (setf *login-message* (display-popup-message + (make-verification-buffer user-code verification-uri) + :style '(:gravity :center) + :timeout nil)) + (add-hook *editor-abort-hook* 'abort-login)) + +(defun abort-login () + (delete-login-message) + (remove-hook *editor-abort-hook* 'abort-login)) + +(defun delete-login-message () + (when *login-message* + (delete-popup-message *login-message*) + (setf *login-message* nil))) + +(define-command copilot-login () () + (unless (copilot::installed-copilot-server-p) + (copilot::copilot-install-server)) + (copilot::setup-client-async + (lambda () + (let* ((response (client:sign-in-initiate (copilot::client))) + (status (gethash "status" response)) + (user-code (gethash "userCode" response)) + (verification-uri (gethash "verificationUri" response)) + (user (gethash "user" response))) + (when (equal status "AlreadySignedIn") + (error 'already-sign-in :message (format nil "Already sign in as ~A" user))) + (copy-to-clipboard user-code) + (start-login user-code verification-uri) + (open-external-file verification-uri) + (redraw-display) + (let ((finished nil)) + (client:sign-in-confirm + (copilot::client) + user-code + :callback (lambda (response) + (send-event (lambda () + (assert (equal "OK" (gethash "status" response))) + (show-message (format nil "Authenticated as ~A" (gethash "user" response)) + :style '(:gravity :center)) + (delete-login-message) + (setf finished t) + (redraw-display))))) + (handler-bind ((editor-abort (lambda (c) + (declare (ignore c)) + (delete-login-message)))) + + (loop :until finished + :do (sit-for 1))) + (copilot::enable-copilot)))))) diff --git a/extensions/copilot/test-commands.lisp b/extensions/copilot/test-commands.lisp new file mode 100644 index 000000000..afb2a243b --- /dev/null +++ b/extensions/copilot/test-commands.lisp @@ -0,0 +1,21 @@ +(defpackage :lem-copilot/test-commands + (:use :cl :lem :lem-copilot/utils) + (:local-nicknames (:client :lem-copilot/client) + (:logger :lem-copilot/logger) + (:copilot :lem-copilot))) +(in-package :lem-copilot/test-commands) + +(define-command test/copilot-document () () + (let ((response (client:request (copilot::client) + "testing/getDocument" + (hash "uri" (copilot::buffer-uri (current-buffer)))))) + (show-message (pretty-json response)) + (assert (equal (gethash "text" response) + (buffer-text (current-buffer)))))) + +(define-command test/copilot-log () () + (let* ((buffer (make-buffer "*copilot-log*" :enable-undo-p nil))) + (erase-buffer buffer) + (with-open-stream (stream (make-buffer-output-stream (buffer-point buffer))) + (logger:write-log stream)) + (pop-to-buffer buffer)))