diff --git a/Eask b/Eask index a245f07d2e..0d36bafd39 100644 --- a/Eask +++ b/Eask @@ -39,6 +39,7 @@ (depends-on "markdown-mode") (depends-on "lv") (depends-on "eldoc") +(depends-on "uuidgen") (development (depends-on "flycheck") diff --git a/clients/lsp-copilot.el b/clients/lsp-copilot.el index 3321575832..b8d66d474e 100644 --- a/clients/lsp-copilot.el +++ b/clients/lsp-copilot.el @@ -25,13 +25,15 @@ ;; LSP client for the Copilot Language Server: ;; https://www.npmjs.com/package/@github/copilot-language-server -;; Package-Requires: (lsp-mode secrets s compile dash cl-lib request company) +;; Package-Requires: (lsp-mode secrets s compile dash cl-lib request uuidgen) ;; Code: (require 'dash) (require 'lsp-mode) (require 's) +(require 'uuidgen) +(require 'lsp-inline-completion) (defgroup lsp-copilot () "Copilot LSP configuration" @@ -91,6 +93,215 @@ The input are the file name and the major mode of the buffer." :path "copilot-language-server" :version lsp-copilot-version)) + +;;; Panel Completion + +(declare-function company--active-p "ext:company") +(declare-function company-cancel "ext:company" (&optional result)) +(declare-function org-forward-heading-same-level "ext:org" (arg &optional invisible-ok)) +(declare-function org-backward-heading-same-level "ext:org" (arg &optional invisible-ok)) +(declare-function org-down-element "ext:org") +(defvar-local lsp-copilot-panel-completion-items nil + "A list of completion items returned by the Panel Completion call") + +(defvar-local lsp-copilot-panel-completion-token nil + "The per-request token") + +(defvar-local lsp-copilot-panel-modification-tick nil + "Modification tick when the panel was called") + +(defun lsp-copilot--panel-accept-item (completing-buffer-name original-tick item) + (when (buffer-live-p (get-buffer completing-buffer-name)) + (with-current-buffer completing-buffer-name + (unless (= original-tick (buffer-chars-modified-tick)) + (user-error "Can not accept the suggestion on a modified buffer. Try copying it")) + + (-let* (((&copilot-ls:PanelCompletionItem? :insert-text :range? :command?) item) + ((start . end) (when range? + (-let (((&RangeToPoint :start :end) range?)) (cons start end))))) + + (unless (and start end) + (error "Server did not provide a range -- Can not insert")) + + (lsp-with-undo-amalgamate + (delete-region start end) + (goto-char start) + (insert insert-text)) + + ;; Post command + (when command? + (lsp--execute-command command?)))))) + +(defun lsp-copilot--panel-accept-suggestion-at-point () + (interactive) + (let ((completing-buffer-name (get-text-property (point) 'lsp-panel-completing-buffer-name)) + (original-tick (get-text-property (point) 'lsp-original-tick)) + (item (get-text-property (point) 'lsp-panel-item))) + (lsp-copilot--panel-accept-item completing-buffer-name original-tick item) + (quit-window))) + +(defun lsp-copilot--panel-accept-button-click (b) + (when-let* ((item (overlay-get b 'lsp-panel-item)) + (completing-buffer-name (overlay-get b 'lsp-panel-completing-buffer-name)) + (original-tick (overlay-get b 'lsp-original-tick))) + (lsp-copilot--panel-accept-item completing-buffer-name original-tick item) + + (quit-window))) + +(defun lsp-copilot--panel-copy-button-click (b) + + (kill-new (lsp:copilot-ls-panel-completion-item-insert-text (overlay-get b 'lsp-panel-item))) + (quit-window)) + +(defun lsp-copilot--panel-forward-suggestion (arg) + (interactive "p") + (org-forward-heading-same-level arg) + (recenter-top-bottom 0) + (org-down-element)) + +(defun lsp-copilot--panel-backward-suggestion (arg) + (interactive "p") + (org-backward-heading-same-level arg) + (recenter-top-bottom 0) + (org-down-element)) + +(define-derived-mode lsp-copilot-panel-buffer-mode org-mode "CopilotPanel" + "A major mode for completions " + :group 'lsp-copilot) + + +(let ((key-bindings '(("C-n" . lsp-copilot--panel-forward-suggestion) + ("C-p" . lsp-copilot--panel-backward-suggestion) + ("C-" . lsp-copilot--panel-accept-suggestion-at-point) + ("q" . quit-window)))) + (dolist (binding key-bindings) + (define-key lsp-copilot-panel-buffer-mode-map (kbd (car binding)) (cdr binding)))) + + +(defcustom lsp-copilot-panel-display-fn #'pop-to-buffer + "Function used to display the panel completions buffer" + :type 'function + :group 'lsp-copilot) + + +(defun lsp-copilot--panel-display-buffer (completing-buffer-name items original-tick) + "Builds and display the panel buffer" + (if (lsp-inline-completion--active-p) + (progn + (lsp-inline-completion-cancel))) + + (when (and (bound-and-true-p company-mode) + (company--active-p)) + (company-cancel)) + + (let ((buf (get-buffer-create (format "*lsp-copilot-panel-results-%s*" completing-buffer-name) ))) + (with-current-buffer buf + (read-only-mode -1) + (erase-buffer) + (fundamental-mode) + + ;; (insert "#+STARTUP: noindent\n\n") + (setq-local org-startup-indented nil) + (cl-loop for item in items + for i from 1 + do + (-let* ((start-point (point)) + ((&copilot-ls:PanelCompletionItem? :insert-text) item) + (heading (format "* Solution %d" i)) + (src (format "#+begin_src %s\n%s\n#+end_src\n" "python" insert-text))) + + (insert heading) + (insert ?\n ?\n) + (insert-button "Accept" + 'action #'lsp-copilot--panel-accept-button-click + 'lsp-original-tick original-tick + 'lsp-panel-item item + 'lsp-panel-completing-buffer-name completing-buffer-name) + + (insert ?\s) + + (insert-button "Copy" + 'action #'lsp-copilot--panel-copy-button-click + 'lsp-original-tick original-tick + 'lsp-panel-item item + 'lsp-panel-completing-buffer-name completing-buffer-name) + + (insert ?\n) + (insert src) + (insert ?\n ?\n) + + (put-text-property start-point (point) 'lsp-original-tick original-tick) + (put-text-property start-point (point) 'lsp-panel-item item) + (put-text-property start-point (point) 'lsp-panel-completing-buffer-name completing-buffer-name))) + + (lsp-delete-all-space t) + (lsp-copilot-panel-buffer-mode) + (read-only-mode +1) + + (goto-char (point-min)) + (org-down-element)) + + (if (get-buffer-window completing-buffer-name 'visible) + (progn + (select-window (get-buffer-window completing-buffer-name 'visible)) + (funcall lsp-copilot-panel-display-fn buf)) + (user-error "The original buffer for completions was not active; not showing panel")))) + + +(defun lsp-copilot-panel-display-buffer () + "Displays a completion panel with the items collected by the last call of lsp-copilot-panel-completion" + (interactive) + + (if lsp-copilot-panel-completion-items + (lsp-copilot--panel-display-buffer (buffer-name) lsp-copilot-panel-completion-items lsp-copilot-panel-modification-tick) + (lsp--error "No completions to display"))) + +(defun lsp-copilot--panel-completions-progress-handler (_ params) + (-let* (((&ProgressParams :token :value) params) + ((action completing-buffer-name panel-completion-token) (s-split " /// " token))) + (pcase action + ;; copilot sends results in the report + ("PANEL-PARTIAL-RESULT" + (when (and (lsp-copilot-ls-panel-completion-items? value) + (buffer-live-p (get-buffer completing-buffer-name))) + (with-current-buffer completing-buffer-name + (when (string-equal panel-completion-token lsp-copilot-panel-completion-token) + (setq-local lsp-copilot-panel-completion-items + (nconc lsp-copilot-panel-completion-items + (seq-into (lsp:copilot-ls-panel-completion-items-items value) 'list))))))) + ("PANEL-WORK-DONE" + (when (and (lsp-work-done-progress-end? value) + (buffer-live-p (get-buffer completing-buffer-name)) + (string-equal (lsp:work-done-progress-end-kind value) "end")) + (with-current-buffer completing-buffer-name + (when (string-equal panel-completion-token lsp-copilot-panel-completion-token) + (lsp-copilot-panel-display-buffer)))))))) + +(defun lsp-copilot-panel-completion () + "Use a Completion Panel to provide suggestions at point" + (interactive) + + (lsp-inline-completion-inhibit-idle-completions) + (lsp-inline-completion-cancel-timer) + + (let* ((token (uuidgen-1)) + (partial-token (format "PANEL-PARTIAL-RESULT /// %s /// %s" (buffer-name) token)) + (done-token (format "PANEL-WORK-DONE /// %s /// %s" (buffer-name) token)) + (params (lsp-make-copilot-ls-panel-completion-params :text-document (lsp--text-document-identifier) + :position (lsp--cur-position) + :partial-result-token partial-token + :work-done-token done-token))) + (setq-local lsp-copilot-panel-modification-tick (buffer-chars-modified-tick)) + (setq-local lsp-copilot-panel-completion-token token) + (setq-local lsp-copilot-panel-completion-items nil) + + ;; call the complation and do not wait for any result -- the completions + ;; will be provided via $/progress notifications + (lsp-request-async "textDocument/copilotPanelCompletion" params #'ignore))) + + +;;; LSP Client + (defun lsp-copilot--find-active-workspaces () "Returns a list of lsp-copilot workspaces" (-some->> (lsp-session) @@ -190,13 +401,18 @@ automatically, browse to %s." user-code verification-uri)) :name "emacs" :version "0.1.0")) +(defun lsp-copilot--progress-callback (workspace params) + (when lsp-progress-function + (funcall lsp-progress-function workspace params)) + + (lsp-copilot--panel-completions-progress-handler workspace params)) + (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 @@ -224,7 +440,7 @@ automatically, browse to %s." user-code verification-uri)) :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))) + ("$/progress" #'lsp-copilot--progress-callback) ("featureFlagsNotification" #'ignore) ("statusNotification" #'ignore) ("didChangeStatus" #'ignore) diff --git a/lsp-inline-completion.el b/lsp-inline-completion.el index 553610cc6b..0fafa2fc2e 100644 --- a/lsp-inline-completion.el +++ b/lsp-inline-completion.el @@ -407,25 +407,59 @@ lsp-inline-completion-mode is active." (defvar-local lsp-inline-completion--idle-timer nil "The idle timer used by lsp-inline-completion-mode.") +(defvar-local lsp-inline-completion--inhibit-idle-timer nil + "Flag to indicate we do not want the timer to show inline completions. Reset on change.") + +;;;###autoload +(defun lsp-inline-completion-inhibit-idle-completions () + (setq lsp-inline-completion--inhibit-idle-timer t)) + +;;;###autoload +(defun lsp-inline-completion-uninhibit-idle-completions () + (setq lsp-inline-completion--inhibit-idle-timer nil)) + +;;;###autoload +(defun lsp-inline-completion-is-inhibited () + lsp-inline-completion--inhibit-idle-timer) + +;;;###autoload +(defun lsp-inline-completion-cancel-timer () + "Cancels the completion idle timer, if set" + (when lsp-inline-completion--idle-timer + (cancel-timer lsp-inline-completion--idle-timer) + (setq lsp-inline-completion--idle-timer nil))) + ;;;###autoload (define-minor-mode lsp-inline-completion-mode "Mode automatically displaying inline completions." :lighter nil (cond ((and lsp-inline-completion-mode lsp--buffer-workspaces) + (add-hook 'after-change-functions #'lsp-inline-completion--uninhibit-on-change nil t) (add-hook 'lsp-on-change-hook #'lsp-inline-completion--after-change nil t)) + (t - (when lsp-inline-completion--idle-timer - (cancel-timer lsp-inline-completion--idle-timer)) + (lsp-inline-completion-cancel-timer) (lsp-inline-completion-cancel) + (remove-hook 'after-change-functions #'lsp-inline-completion--uninhibit-on-change t) (remove-hook 'lsp-on-change-hook #'lsp-inline-completion--after-change t)))) +(defun lsp-inline-completion--uninhibit-on-change (&rest _) + "Resets the uninhibit flag. " + + ;; Must be done in after-change-functions instead of lsp-on-change-hook, + ;; because LSP's hook happens after lsp-idle-delay. If the user calls some + ;; function that sets the inhibit flag to t *before* the idle delay, we may + ;; end up overriding the flag." + (lsp-inline-completion-uninhibit-idle-completions)) + (defun lsp-inline-completion--maybe-display (original-buffer original-point) ;; This is executed on an idle timer -- ensure state did not change before ;; displaying - (when (and (buffer-live-p original-buffer) + (when (and (not (lsp-inline-completion-is-inhibited)) + (buffer-live-p original-buffer) (eq (current-buffer) original-buffer) (eq (point) original-point) (--none? (funcall it) lsp-inline-completion-inhibit-predicates)) @@ -439,8 +473,7 @@ lsp-inline-completion-mode is active." ;; modified in the meantime! Use the values in lsp--after-change-vals to ;; ensure this. - (when lsp-inline-completion--idle-timer - (cancel-timer lsp-inline-completion--idle-timer)) + (lsp-inline-completion-cancel-timer) (when (and lsp-inline-completion-mode lsp--buffer-workspaces) (let ((original-buffer (plist-get lsp--after-change-vals :buffer)) @@ -457,6 +490,7 @@ lsp-inline-completion-mode is active." (when (and lsp-inline-completion-enable (lsp-feature? "textDocument/inlineCompletion")) (lsp-inline-completion-mode)))) + ;; Company default integration diff --git a/lsp-mode.el b/lsp-mode.el index a99a944074..932bbee6a7 100644 --- a/lsp-mode.el +++ b/lsp-mode.el @@ -1429,30 +1429,51 @@ Symlinks are not followed." ;; compat (if (version< emacs-version "29.1") - ;; Undo macro probably introduced in 29.1 - (defmacro lsp-with-undo-amalgamate (&rest body) - "Like `progn' but perform BODY with amalgamated undo barriers. + (progn + ;; Undo macro probably introduced in 29.1 + (defmacro lsp-with-undo-amalgamate (&rest body) + "Like `progn' but perform BODY with amalgamated undo barriers. This allows multiple operations to be undone in a single step. When undo is disabled this behaves like `progn'." - (declare (indent 0) (debug t)) - (let ((handle (make-symbol "--change-group-handle--"))) - `(let ((,handle (prepare-change-group)) - ;; Don't truncate any undo data in the middle of this, - ;; otherwise Emacs might truncate part of the resulting - ;; undo step: we want to mimic the behavior we'd get if the - ;; undo-boundaries were never added in the first place. - (undo-outer-limit nil) - (undo-limit most-positive-fixnum) - (undo-strong-limit most-positive-fixnum)) - (unwind-protect + (declare (indent 0) (debug t)) + (let ((handle (make-symbol "--change-group-handle--"))) + `(let ((,handle (prepare-change-group)) + ;; Don't truncate any undo data in the middle of this, + ;; otherwise Emacs might truncate part of the resulting + ;; undo step: we want to mimic the behavior we'd get if the + ;; undo-boundaries were never added in the first place. + (undo-outer-limit nil) + (undo-limit most-positive-fixnum) + (undo-strong-limit most-positive-fixnum)) + (unwind-protect + (progn + (activate-change-group ,handle) + ,@body) (progn - (activate-change-group ,handle) - ,@body) + (accept-change-group ,handle) + (undo-amalgamate-change-group ,handle)))))) + + ;; delete-all-space function introduced in 29 + (defun lsp-delete-all-space (&optional backward-only) + "Delete all spaces, tabs, and newlines around point. +If BACKWARD-ONLY is non-nil, delete them only before point." + (interactive "*P") + (let ((chars " \t\r\n") + (orig-pos (point))) + (delete-region + (if backward-only + orig-pos (progn - (accept-change-group ,handle) - (undo-amalgamate-change-group ,handle)))))) - (defalias 'lsp-with-undo-amalgamate 'with-undo-amalgamate)) + (skip-chars-forward chars) + (constrain-to-field nil orig-pos t))) + (progn + (skip-chars-backward chars) + (constrain-to-field nil orig-pos)))))) + ;;29.1+ + (defalias 'lsp-with-undo-amalgamate 'with-undo-amalgamate) + (defalias 'lsp-delete-all-space 'delete-all-space)) + (defun lsp--merge-results (results method) "Merge RESULTS by filtering the empty hash-tables and merging diff --git a/lsp-protocol.el b/lsp-protocol.el index 421a30d54f..fe84a2d344 100644 --- a/lsp-protocol.el +++ b/lsp-protocol.el @@ -473,7 +473,10 @@ See `-let' for a description of the destructuring mechanism." (lsp-interface (copilot-ls:SignInInitiateResponse (:status :userCode :verificationUri :expiresIn :interval :user) nil) (copilot-ls:SignInConfirmResponse (:status :user)) - (copilot-ls:CheckStatusResponse (:status :user))) + (copilot-ls:CheckStatusResponse (:status :user)) + (copilot-ls:PanelCompletionParams (:textDocument :position :partialResultToken :workDoneToken)) + (copilot-ls:PanelCompletionItem (:insertText) (:range :command)) + (copilot-ls:PanelCompletionItems (:items) nil)) ;; begin autogenerated code