February 22, 2023

Setting up Github Copilot in Emacs

Quickstart

Either follow the instructions of zerolfx/copilot.el or use a ready-to-roll config provided at rksm/copilot-emacsd.

This is a short walkthrough describing my current Emacs-Copilot setup. Copilot is primarily advertised as a VSCode utility but actually it works really well in Emacs. Given the ease of adjustments (e.g. when to see completions) I would even argue Emacs might provide a better experience.

Table of Contents

Changelog

  • 2023-03-20: zerolfx/copilot.el has been updated, so switching to only referencing it instead of my fork. Thank you Jason H.!

Intro & Disclaimer

Github Copilot is a tool that uses machine learning to generate text completions, primarily meant for source code, even though it works surprisingly well for all kinds of texts. It is a commercial Microsoft product, though it is currently free to use if you have a few public Github repositories that have garnered some stars. It is based on OpenAI’s Codex model which itself is derived from the GPT-3 large language model (LLM). So in some sense it is ChatGPT for your editor.

LLMs that are trained on open (source) data generally, and Copilot in particular, are controversial. Among other things, many people have expressed concerns about the questionable legal grounds that these tools are build on, e.g. there is currently a pending lawsuit focusing on potential violations of open source licenses. So let me say that I’m well aware of the controversy and I would suggest careful use of Copilot, in particular when used in code bases that are not your own.

That said, it can be an astonishingly useful tool. For example, when learning languages or frameworks or dealing with new libraries are APIs, it is surprisingly good at providing guidance that often saves you many trips to the documentation. Moreover, it is really good at detecting certain patterns in your code while also seeing variations and suggesting new code based on that — clever copy/paste on steroids. Also tests are a good area where Copilot can be helpful.

And of course check the generated code. That thing will make mistakes. It is surprisingly good at getting the syntax right (though sometimes it won’t correctly close parentheses) but be aware of logical issues, anti-patterns, or outdated code.

Copilot API & integration

Even though Copilot is primarily a VSCode utility, making it work in Emacs is fairly straightforward. In essence it is not much different than a language server. The VSCode extension is not open source but since it is implemented in JavaScript you can extract the vsix package as a zip file and get hold of the JS files. As far as I know, the copilot.vim plugin was the first non-VSCode integration that used that approach. The worker.js file that is part of the vsix extension can be started as a node.js process that will read JSON-RPC data from stdin. There are a number of commands that are implemented using this format, here is the current full list:

  • getCompletions
  • getCompletionsCycling
  • getPanelCompletions
  • getVersion
  • setEditorInfo
  • checkStatus
  • signInInitiate
  • signInConfirm
  • signOut
  • notifyShown
  • notifyAccepted
  • notifyRejected
  • telemetry/exception
  • testing/createContext
  • testing/alwaysAuth
  • testing/neverAuth
  • testing/useTestingToken
  • testing/setCompletionDocuments
  • testing/setPanelCompletionDocuments
  • testing/triggerShowMessageRequest
  • testing/getDocument
  • debug/verifyState
  • debug/verifyCertificate
  • debug/verifyWorkspaceState

An editor like Emacs or VIM can start the worker in a subprocess and then interact with, sending JSON messages and reading JSON responses back via stdout.

Given that VSCode uses the exact same interface and from everything I’ve tried so far, I think it is safe to assume that using Copilot with Emacs is in no way inferior. On the contrary, as shown below, customizing the way Copilot works is actually more convenient (at list if you know some elisp :). I’m particularly mentioning that because some folks seem to believe that this is not the case. Should I be wrong on this (now or later), please let me know. It’s annoying that not event the Copilot agent isn’t open source (without open source none of this would be possible!) but it is not that complicated to figure out what it does. Which in turn means that other clients can catch up.

Copilot Emacs packages

On Github I have found three Emacs packages that implement Copilot integration:

  • zerolfx/copilot.el seems by far the most popular one and is the one I’m using below. It provides completions through an overlay, similar to the VSCodes extension.
  • fkr-0/flight-attendant.el might be inactive, the last commit was a year ago. I have not tried it.
  • tyler-dodge/eglot-copilot/ seems to be actively developed but has no documentation. It uses eglot for managing the subprocess and counsel for completions.

Setup & Customizations

You can find a fully working config building on zerolfx/copilot.el that can also be run standalone at rksm/copilot-emacsd. In the following I explain some of the customizations.

zerolfx/copilot.el is not on MELPA so you have to install it through straight.el or other means (see the README). I typically add an Emacs package like that as a submodule to my config:

git submodule add https://github.com/zerolfx/copilot.el
git submodule update --init

The packages required for it to work are: s, dash, editorconfig (and I also use company, use-package and Emacs built-in cl package here as I find those very helpful). Install them like this:

(require 'cl)
(let ((pkg-list '(use-package
		          s
		          dash
		          editorconfig
                  company)))
  (package-initialize)
  (when-let ((to-install (map-filter (lambda (pkg _) (not (package-installed-p pkg))) pkg-list)))
    (package-refresh-contents)
    (mapc (lambda (pkg) (package-install pkg)) pkg-list)))

Assuming the above git submodule was cloned into copilot.el/, we can now load that package:

(use-package copilot
  :load-path (lambda () (expand-file-name "copilot.el" user-emacs-directory))
  ;; don't show in mode line
  :diminish)

You can now run M-x copilot-login to authenticate with your Github account (that needs to have a subscription to the Copilot product) followed by M-x global-copilot-mode to activate Copilot everywhere.

Restricting when to show completions

global-copilot-mode will sometimes be a bit too eager, so we disable in some modes completely:

(defun rk/no-copilot-mode ()
  "Helper for `rk/no-copilot-modes'."
  (copilot-mode -1))

(defvar rk/no-copilot-modes '(shell-mode
                              inferior-python-mode
                              eshell-mode
                              term-mode
                              vterm-mode
                              comint-mode
                              compilation-mode
                              debugger-mode
                              dired-mode-hook
                              compilation-mode-hook
                              flutter-mode-hook
                              minibuffer-mode-hook)
  "Modes in which copilot is inconvenient.")

(defun rk/copilot-disable-predicate ()
  "When copilot should not automatically show completions."
  (or rk/copilot-manual-mode
      (member major-mode rk/no-copilot-modes)
      (company--active-p)))

(add-to-list 'copilot-disable-predicates #'rk/copilot-disable-predicate)

Then, it is also convenient to have the overlays not appear automatically but on-demand:

(defvar rk/copilot-manual-mode nil
  "When `t' will only show completions when manually triggered, e.g. via M-C-<return>.")

(defun rk/copilot-change-activation ()
  "Switch between three activation modes:
- automatic: copilot will automatically overlay completions
- manual: you need to press a key (M-C-<return>) to trigger completions
- off: copilot is completely disabled."
  (interactive)
  (if (and copilot-mode rk/copilot-manual-mode)
      (progn
        (message "deactivating copilot")
        (global-copilot-mode -1)
        (setq rk/copilot-manual-mode nil))
    (if copilot-mode
        (progn
          (message "activating copilot manual mode")
          (setq rk/copilot-manual-mode t))
      (message "activating copilot mode")
      (global-copilot-mode))))

(define-key global-map (kbd "M-C-<escape>") #'rk/copilot-change-activation)

M-C-<escape> will now cycle between three states automatic, manual and off.

Customizing keys

Copilot-specific

I like my keybindings to be somewhat consistent and have assigned M-C-... (Alt + Control + some other key) to Copilot related commands:

(defun rk/copilot-complete-or-accept ()
  "Command that either triggers a completion or accepts one if one
is available. Useful if you tend to hammer your keys like I do."
  (interactive)
  (if (copilot--overlay-visible)
      (progn
        (copilot-accept-completion)
        (open-line 1)
        (next-line))
    (copilot-complete)))

(define-key copilot-mode-map (kbd "M-C-<next>") #'copilot-next-completion)
(define-key copilot-mode-map (kbd "M-C-<prior>") #'copilot-previous-completion)
(define-key copilot-mode-map (kbd "M-C-<right>") #'copilot-accept-completion-by-word)
(define-key copilot-mode-map (kbd "M-C-<down>") #'copilot-accept-completion-by-line)
(define-key global-map (kbd "M-C-<return>") #'rk/copilot-complete-or-accept)

Tab key

If you also want to use the <tab> key for completion but still keep the normal functionality on it, do:

(defun rk/copilot-tab ()
  "Tab command that will complet with copilot if a completion is
available. Otherwise will try company, yasnippet or normal
tab-indent."
  (interactive)
  (or (copilot-accept-completion)
      (company-yasnippet-or-completion)
      (indent-for-tab-command)))

(define-key global-map (kbd "<tab>") #'rk/copilot-tab)

Ctrl-g / cancel

I like to cancel commands with C-g. This does not work out of the box with canceling Copilot completions, be we can do:

(defun rk/copilot-quit ()
  "Run `copilot-clear-overlay' or `keyboard-quit'. If copilot is
cleared, make sure the overlay doesn't come back too soon."
  (interactive)
  (condition-case err
      (when copilot--overlay
        (lexical-let ((pre-copilot-disable-predicates copilot-disable-predicates))
          (setq copilot-disable-predicates (list (lambda () t)))
          (copilot-clear-overlay)
          (run-with-idle-timer
           1.0
           nil
           (lambda ()
             (setq copilot-disable-predicates pre-copilot-disable-predicates)))))
    (error handler)))

(advice-add 'keyboard-quit :before #'rk/copilot-quit)

And that should be it!

Thanks to all the awesome Emacs hackers who made all this possible!

© Robert Krahn 2009-2023