Emacs 29 has made great strides toward allowing users to craft their own Integrated Development Environment (IDE) using its built-in packages. This transformation didn't happen overnight; it's the culmination of Emacs' continuous evolution. The refactor of Flymake, a robust framework for code diagnostics, in Emacs 26 marks the beginning of a new era for Emacs as an IDE. Now, five years and three major versions later, I think the journey has reached a zenith with the built-in support for the Language Server Protocol (LSP) via Eglot.

I want to see how closely I can configure my init.el to get an IDE-like experience using only the tools Emacs core provides for me.

What does it mean to be an IDE?

I'll use VS Code as the paragon of a programmable IDE. To find out what features make it special, we'll see what Microsoft calls out as important:

Visual Studio Code is a free coding editor that helps you start coding quickly. Use it to code in any programming language, without switching editors. Visual Studio Code has support for many languages, including Python, Java, C++, JavaScript, and more.

To start with, we need built-in support for popular programming languages. For my example, I will build an IDE for Ruby, but expanding the functionality to any of the languages mentioned above is trivial.

### Collaborate and code remotely

Work together remotely with your teachers or classmates using the free Live Share extension. Edit and debug your code in real time, and use the chat and call features to ask questions or discuss ideas together.

This one I'll have to skip. Emacs aims to be a local first code editor with no reliance on external systems or servicesโ€ฆ and, well, Live Share requires someone to spend lots of money to maintain servers. This is well outside Emacs' or Emacs users' wheelhouse; I'll take the L here.

As it turns out, I misremembered it, and VS Code does not have Live Share baked into it.

### Code to learn

New to coding? Visual Studio Code highlights keywords in your code in different colors to help you easily identify coding patterns and learn faster. You can also take advantage of features like IntelliSense and Peek Definition

So, to clarify, our language support should include syntax highlighting and some intellisense capabilities.

### Fix errors as you code

As you code, Visual Studio Code gives you suggestions to complete lines of code and quick fixes for common mistakes.

And then we'll need a code-diagnostics, linter.

You can also use the debugger in VS Code to step through each line of code and understand what is happening. Check out guides on how to use the debugger if you're coding in Python, Java, and JavaScript/TypeScript/Node.js.

Like VS Code, Emacs doesn't have built-in support for debugging Ruby, but it does have some built-in debugging capabilities.

### Make it yours with custom themes and colors

You can change the look and feel of VS Code by picking your favorite fonts and icons and choosing from hundreds of color themes. Check out this video on personalizing VS Code.

We should be able to make Emacs look pretty.

### Compare changes in your code

Use the built-in source control to save your work over time so you don't lose progress. See a graphical side-by-side view to compare versions of your code from different points in time. Check out this quick video on how to get a side-by-side "diff".

Finally, we should be able to get diffs of the changes we make and we should also support version control.

So, to summarize, our IDE should:

  1. Support editing in major languages:

    • Have syntax highlighting

    • Have code diagnostics

    • Have IntelliSense

  2. Make Emacs look pretty(er)

  3. Built-in support for version control

    • including built-in support for seeing changes made (diffing)

In The Beginningโ€ฆ

A plain emacs with no settings applied to it looking at a Rails Controller file Plain Emacs

โ€ฆthere was use-package, a lisp macro that allows you to configure Emacs and its libraries and packages declaratively. use-package is a library that has been around for over 10 years. And it has gained such a foothold into the Emacs community that it landed in Emacs core for the release of Emacs 29.

If we look at this code snippet, it says that when Emacs is initialized, :init, it should load the theme some-theme. Setting :ensure to nil then tells Emacs not to download from the package repositories defined in package-archives. use-package can do much more, but you'll have to read elsewhere for that.

  (use-package emacs
   :ensure nil
   :init
   (load-theme 'some-theme))

Theme and Aesthetics: More Than Just Lipstick on a Pig

I'm a shallow person, and I find it hard to use something when it looks ugly. The motivation just isn't there to use something that is an eyesore. So, let's fix that. Unfortunately, we're a bit limited. Emacs has only a handful of themes, and none are particularly charming.

Themes

theme palette
adwaita /images/emacs-ide/palettes/adwaita-background.svg /images/emacs-ide/palettes/adwaita-font-lock-constant-face.svg /images/emacs-ide/palettes/adwaita-font-lock-doc-face.svg /images/emacs-ide/palettes/adwaita-font-lock-keyword-face.svg /images/emacs-ide/palettes/adwaita-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/adwaita-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/adwaita-font-lock-string-face.svg /images/emacs-ide/palettes/adwaita-font-lock-type-face.svg /images/emacs-ide/palettes/adwaita-font-lock-variable-name-face.svg
deeper-blue /images/emacs-ide/palettes/deeper-blue-background.svg /images/emacs-ide/palettes/deeper-blue-font-lock-constant-face.svg /images/emacs-ide/palettes/deeper-blue-font-lock-doc-face.svg /images/emacs-ide/palettes/deeper-blue-font-lock-keyword-face.svg /images/emacs-ide/palettes/deeper-blue-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/deeper-blue-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/deeper-blue-font-lock-string-face.svg /images/emacs-ide/palettes/deeper-blue-font-lock-type-face.svg /images/emacs-ide/palettes/deeper-blue-font-lock-variable-name-face.svg
dichromacy /images/emacs-ide/palettes/dichromacy-background.svg /images/emacs-ide/palettes/dichromacy-font-lock-constant-face.svg /images/emacs-ide/palettes/dichromacy-font-lock-doc-face.svg /images/emacs-ide/palettes/dichromacy-font-lock-keyword-face.svg /images/emacs-ide/palettes/dichromacy-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/dichromacy-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/dichromacy-font-lock-string-face.svg /images/emacs-ide/palettes/dichromacy-font-lock-type-face.svg /images/emacs-ide/palettes/dichromacy-font-lock-variable-name-face.svg
light-blue /images/emacs-ide/palettes/light-blue-background.svg /images/emacs-ide/palettes/light-blue-font-lock-constant-face.svg /images/emacs-ide/palettes/light-blue-font-lock-doc-face.svg /images/emacs-ide/palettes/light-blue-font-lock-keyword-face.svg /images/emacs-ide/palettes/light-blue-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/light-blue-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/light-blue-font-lock-string-face.svg /images/emacs-ide/palettes/light-blue-font-lock-type-face.svg /images/emacs-ide/palettes/light-blue-font-lock-variable-name-face.svg
leuven-dark /images/emacs-ide/palettes/leuven-dark-background.svg /images/emacs-ide/palettes/leuven-dark-font-lock-constant-face.svg /images/emacs-ide/palettes/leuven-dark-font-lock-doc-face.svg /images/emacs-ide/palettes/leuven-dark-font-lock-keyword-face.svg /images/emacs-ide/palettes/leuven-dark-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/leuven-dark-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/leuven-dark-font-lock-string-face.svg /images/emacs-ide/palettes/leuven-dark-font-lock-type-face.svg /images/emacs-ide/palettes/leuven-dark-font-lock-variable-name-face.svg
leuven /images/emacs-ide/palettes/leuven-background.svg /images/emacs-ide/palettes/leuven-font-lock-constant-face.svg /images/emacs-ide/palettes/leuven-font-lock-doc-face.svg /images/emacs-ide/palettes/leuven-font-lock-keyword-face.svg /images/emacs-ide/palettes/leuven-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/leuven-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/leuven-font-lock-string-face.svg /images/emacs-ide/palettes/leuven-font-lock-type-face.svg /images/emacs-ide/palettes/leuven-font-lock-variable-name-face.svg
manoj-dark /images/emacs-ide/palettes/manoj-dark-background.svg /images/emacs-ide/palettes/manoj-dark-font-lock-constant-face.svg /images/emacs-ide/palettes/manoj-dark-font-lock-doc-face.svg /images/emacs-ide/palettes/manoj-dark-font-lock-keyword-face.svg /images/emacs-ide/palettes/manoj-dark-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/manoj-dark-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/manoj-dark-font-lock-string-face.svg /images/emacs-ide/palettes/manoj-dark-font-lock-type-face.svg /images/emacs-ide/palettes/manoj-dark-font-lock-variable-name-face.svg
misterioso /images/emacs-ide/palettes/misterioso-background.svg /images/emacs-ide/palettes/misterioso-font-lock-constant-face.svg /images/emacs-ide/palettes/misterioso-font-lock-doc-face.svg /images/emacs-ide/palettes/misterioso-font-lock-keyword-face.svg /images/emacs-ide/palettes/misterioso-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/misterioso-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/misterioso-font-lock-string-face.svg /images/emacs-ide/palettes/misterioso-font-lock-type-face.svg /images/emacs-ide/palettes/misterioso-font-lock-variable-name-face.svg
modus-operandi /images/emacs-ide/palettes/modus-operandi-background.svg /images/emacs-ide/palettes/modus-operandi-font-lock-constant-face.svg /images/emacs-ide/palettes/modus-operandi-font-lock-doc-face.svg /images/emacs-ide/palettes/modus-operandi-font-lock-keyword-face.svg /images/emacs-ide/palettes/modus-operandi-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/modus-operandi-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/modus-operandi-font-lock-string-face.svg /images/emacs-ide/palettes/modus-operandi-font-lock-type-face.svg /images/emacs-ide/palettes/modus-operandi-font-lock-variable-name-face.svg
modus-vivendi /images/emacs-ide/palettes/modus-vivendi-background.svg /images/emacs-ide/palettes/modus-vivendi-font-lock-constant-face.svg /images/emacs-ide/palettes/modus-vivendi-font-lock-doc-face.svg /images/emacs-ide/palettes/modus-vivendi-font-lock-keyword-face.svg /images/emacs-ide/palettes/modus-vivendi-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/modus-vivendi-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/modus-vivendi-font-lock-string-face.svg /images/emacs-ide/palettes/modus-vivendi-font-lock-type-face.svg /images/emacs-ide/palettes/modus-vivendi-font-lock-variable-name-face.svg
tango-dark /images/emacs-ide/palettes/tango-dark-background.svg /images/emacs-ide/palettes/tango-dark-font-lock-constant-face.svg /images/emacs-ide/palettes/tango-dark-font-lock-doc-face.svg /images/emacs-ide/palettes/tango-dark-font-lock-keyword-face.svg /images/emacs-ide/palettes/tango-dark-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/tango-dark-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/tango-dark-font-lock-string-face.svg /images/emacs-ide/palettes/tango-dark-font-lock-type-face.svg /images/emacs-ide/palettes/tango-dark-font-lock-variable-name-face.svg
tango /images/emacs-ide/palettes/tango-background.svg /images/emacs-ide/palettes/tango-font-lock-constant-face.svg /images/emacs-ide/palettes/tango-font-lock-doc-face.svg /images/emacs-ide/palettes/tango-font-lock-keyword-face.svg /images/emacs-ide/palettes/tango-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/tango-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/tango-font-lock-string-face.svg /images/emacs-ide/palettes/tango-font-lock-type-face.svg /images/emacs-ide/palettes/tango-font-lock-variable-name-face.svg
tsdh-dark /images/emacs-ide/palettes/tsdh-dark-background.svg /images/emacs-ide/palettes/tsdh-dark-font-lock-constant-face.svg /images/emacs-ide/palettes/tsdh-dark-font-lock-doc-face.svg /images/emacs-ide/palettes/tsdh-dark-font-lock-keyword-face.svg /images/emacs-ide/palettes/tsdh-dark-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/tsdh-dark-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/tsdh-dark-font-lock-string-face.svg /images/emacs-ide/palettes/tsdh-dark-font-lock-type-face.svg /images/emacs-ide/palettes/tsdh-dark-font-lock-variable-name-face.svg
tsdh-light /images/emacs-ide/palettes/tsdh-light-background.svg /images/emacs-ide/palettes/tsdh-light-font-lock-constant-face.svg /images/emacs-ide/palettes/tsdh-light-font-lock-doc-face.svg /images/emacs-ide/palettes/tsdh-light-font-lock-keyword-face.svg /images/emacs-ide/palettes/tsdh-light-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/tsdh-light-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/tsdh-light-font-lock-string-face.svg /images/emacs-ide/palettes/tsdh-light-font-lock-type-face.svg /images/emacs-ide/palettes/tsdh-light-font-lock-variable-name-face.svg
wheatgrass /images/emacs-ide/palettes/wheatgrass-background.svg /images/emacs-ide/palettes/wheatgrass-font-lock-constant-face.svg /images/emacs-ide/palettes/wheatgrass-font-lock-doc-face.svg /images/emacs-ide/palettes/wheatgrass-font-lock-keyword-face.svg /images/emacs-ide/palettes/wheatgrass-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/wheatgrass-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/wheatgrass-font-lock-string-face.svg /images/emacs-ide/palettes/wheatgrass-font-lock-type-face.svg /images/emacs-ide/palettes/wheatgrass-font-lock-variable-name-face.svg
whiteboard /images/emacs-ide/palettes/whiteboard-background.svg /images/emacs-ide/palettes/whiteboard-font-lock-constant-face.svg /images/emacs-ide/palettes/whiteboard-font-lock-doc-face.svg /images/emacs-ide/palettes/whiteboard-font-lock-keyword-face.svg /images/emacs-ide/palettes/whiteboard-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/whiteboard-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/whiteboard-font-lock-string-face.svg /images/emacs-ide/palettes/whiteboard-font-lock-type-face.svg /images/emacs-ide/palettes/whiteboard-font-lock-variable-name-face.svg
wombat /images/emacs-ide/palettes/wombat-background.svg /images/emacs-ide/palettes/wombat-font-lock-constant-face.svg /images/emacs-ide/palettes/wombat-font-lock-doc-face.svg /images/emacs-ide/palettes/wombat-font-lock-keyword-face.svg /images/emacs-ide/palettes/wombat-font-lock-preprocessor-face.svg /images/emacs-ide/palettes/wombat-font-lock-regexp-grouping-construct.svg /images/emacs-ide/palettes/wombat-font-lock-string-face.svg /images/emacs-ide/palettes/wombat-font-lock-type-face.svg /images/emacs-ide/palettes/wombat-font-lock-variable-name-face.svg

I like dark themes, and wombat has the least garish of colours for dark themes, so I'll go with that.

(load-theme 'wombat)

If you prefer a light theme, you can't go wrong with dichromacy.

(load-theme 'dichromacy)

Facing the Music

Programming fonts are a very personal choice. You can change the default font by using set-face-attribute. Where face, is kind of like a CSS Class. It's a named collection of graphical attributes for display, default in our case that contains information on what font to render and how it should look. This function takes in a set of arguments: the face's name, what frame this face should be applied to, and the list of attributes to apply to the face. We only really care about setting the font and height attributes for the default face. If you want to refine things further, you can find all the face attributes here.

  ;; (set-face-attribute face frame &rest arguments)
  (use-package emacs
  ;;...
    :init
    (load-theme 'wombat)
    (set-face-attribute 'default nil :font "CaskaydiaCove Nerd Font Mono" :height 160))

Other minor UI tweaks

I run Emacs in GUI mode, and I can't help but feel that the scrollbars are an eyesore. We can check that scroll-bar-mode is enabled and then turn it off.

  (when scroll-bar-mode
    (scroll-bar-mode -1))

The toolbar takes up a lot of space and doesn't add much to the equation if we keep the menu bar in play, so I also disable that.

  (tool-bar-mode -1)

It's often encouraged to turn off the menu bar as well, but if you ever get lost in Emacs, it's best to have menu-bar-mode enabled to see what you can do in the current buffer

  (menu-bar-mode -1)

By default, calling M-x is a bit bare. However, emacs comes with a couple of modes to help with that. There is the ancient ido-mode, the more recent icomplete-mode, and the slight enhancement to icomplete-mode called fido-mode. By default, both of these modes expand all the options horizontally in the minibuffer. Still, they have alternatives that I prefer, which expand the options vertically, called icomplete-vertical-mode and fido-vertical-mode.

  (fido-vertical-mode)

End of the beginning

This still isn't the prettiest-looking editor, but I've improved the situation. If I combine all the configuration snippets, we can start our config file at $HOME/.emacs.d/init.el

  (use-package emacs
    :init
    (tool-bar-mode -1)
    (when scroll-bar-mode
      (scroll-bar-mode -1))
    (load-theme 'wombat)
    (set-face-attribute 'default nil :font "CaskaydiaCove Nerd Font Mono" :height 160)
    (fido-vertical-mode)
    :custom
    (treesit-language-source-alist
     '((ruby "https://github.com/tree-sitter/tree-sitter-ruby"))))

Major Modes and Highlighting

Now that things are looking better, let's learn how to customize major modes. A major mode describes the behaviour associated with a buffer. This behaviour generally consists of syntax highlighting, cursor movement, and some default keybindings/interactions for buffers related to source files. ruby-ts-mode is Emacs' major mode that utilizes tree-sitter for syntax-highlighting.

Most major modes in Emacs that are tree-sitter based have -ts- within the name. So theoretically, you could call ruby-ts-mode and have tree-sitter based ruby syntax highlighting for your files.

  (use-package ruby-ts-mode
    :mode "\\.rb\\'"
    :mode "Rakefile\\'"
    :mode "Gemfile\\'")

I use the :mode keyword to specify which file types should be controlled by the ruby-ts-mode. In this example, any file ending in ".rb" and any file called "Rakefile" or "Gemfile" should activate the ruby-ts major mode.

Installing a tree-sitter grammar

Unfortunately, using a tree-sitter major mode is not quite that simple. First, ensure that Emacs was compiled with tree-sitter support using the --with-tree-sitter flag. Second, although Emacs can utilize tree-sitter grammar and parsers, it does not install them for you. Instead, you need to create an alist to treesit-language-source-alist. This alist should be a cons cell of language and git repo for the tree-sitter parser.

So, for Ruby, that would look like

  (use-package emacs
    ;;...
    :custom
    (treesit-language-source-alist
     '((ruby "https://github.com/tree-sitter/tree-sitter-ruby"))))

Then, you must run the command treesit-install-language-grammar and select the language you want to install. IE: M-x treesit-install-language-grammar RET ruby RET.

For a more in-depth look into how to set up tree-sitter for Emacs 29, see Mickey Peterson's article.

Bindings

Now that we have a working ts-mode, what else can Emacs do for us? It will also add keybindings to simplify common operations for Ruby and many other languages.

Alongside Emacs' regular keybindings, ruby-ts-mode adds the following:

Key Bindings Interactive function Description
C-M-q prog-indent-sexp Indent the expression after point.
C-c ' ruby-toggle-string-quotes Toggle string literal quoting between single and double.
C-c C-f ruby-find-library-file Visit a library file denoted by FEATURE-NAME.
C-c { ruby-toggle-block Toggle block type from do-end to braces or back.
M-q prog-fill-reindent-defun Refill or reindent the paragraph or defun that contains the point.

You can explore what keybindings are available for a buffer by typing M-x describe-mode or pressing C-h m.

You can also set some key bindings yourself. For instance, what about jumping to the beginning and end of functions? Here, I use C-c because that is the common prefix for mode-specific key-bindings, then I use r for ruby, and then b for beginning or e for end of defun.

  (define-key ruby-ts-mode-map (kbd "C-c r b") 'treesit-beginning-of-defun)
  (define-key ruby-ts-mode-map (kbd "C-c r e") 'treesit-end-of-defun)

Or you can use bind-key to simplify this.

  (use-package bind-key)

  (use-package ruby-ts-mode
    :bind (:map ruby-ts-mode-map
                ("C-c r b" . treesit-beginning-of-defun)
                ("C-c r e" . treesit-end-of-defun))
    ;;...
    )

And if you forget what these key chords, or any key chords, you can use C-h k to describe a key chord. For example, pressing C-h k + C-c r b in ruby-ts-mode opens up a buffer saying

ruby-beginning-of-defun is an interactive and natively compiled function defined in ruby-mode.el.gz

Customizing Ruby Mode

To find a complete list of customizable attributes for ruby-ts-mode, you can search by calling customize-group, for example, M-x customize-group RET ruby RET. But for now, we'll focus on whitespace:

  (use-package ruby-ts-mode
    ;;...
    :custom
    (ruby-indent-level 2)
    (ruby-indent-tabs-mode nil))

You can also tell Emacs to enable minor modes like subword-mode when your major mode starts up. I'll define a cons cell of the major-minor mode pairs (major-mode . minor-mode) alongside the :hook keyword

  (use-package ruby-ts-mode
    :hook (ruby-ts-mode . subword-mode))

The subword minor mode replaces the basic word-oriented movement and editing commands with variants that recognize subwords in [words with mixed upper and lowercase characters] and treat them as separate words

Putting it all together

With those tweaks and adjustments, we can define our ruby config like so:

  (use-package ruby-ts-mode
    :mode "\\.rb\\'"
    :mode "Rakefile\\'"
    :mode "Gemfile\\'"
    :hook (ruby-ts-mode . subword-mode)
    :bind (:map ruby-ts-mode-map
                ("C-c r b" . 'treesit-beginning-of-defun)
                ("C-c r e" . 'treesit-end-of-defun))
    :custom
    (ruby-indent-level 2)
    (ruby-indent-tabs-mode nil))
A wombatified Emacs with the menu-bar and scroll-bars removed looking at a Rails Controller file

Codes sense and completion

Language Servers have becomes the industry standard for getting IntelliSense like behaviour from your editor. And, with the release of version 29, Emacs has built-in support for LSP with Eglot, which stands for Emacs Polyglot.

Some of the features Eglot provides:

  • At-point documentation

  • On-the-fly diagnostic annotations

  • Finding definitions and uses of identifiers

  • Buffer navigation

  • completion of symbol at point

  • automatic code formatting

  • integration with popular third-party packages including yasnippet, markdown-mode, company-mode or corfu.

  • support for over 40 language servers

Luckily, Eglot is easy to set up. We can use the prog-mode-hook and Eglot's eglot-ensure function to attempt to start a language server for all programming related buffers.

Prog mode is a basic major mode for buffers containing programming language source code. All of the major modes for programming languages that are built into Emacs are derived from it.

  (use-package eglot
    :hook (prog-mode . eglot-ensure))

Eglot comes with several of features, and some of these features integrate with other libraries/packages of Emacs. I've outlined the features of Eglot that I will use and the library dependency, if any, it relies on.

Feature Dependency
complete symbol at point completion-at-point
code formatting
At-point documentation eldoc
on-the-fly eglotโ€“diagnostics flymake
buffer-navigation imenu
jump to definition/find useage xref

It is up to you to ensure your language server is installed. Eglot will not install it for you.

Adding Documentation

In general, I think it's best to enable eldoc everywhere

Eldoc, which started out as emacs-lips documentation, is Emacs' documentation library. When enabled, it shows either the function's documentation or, barring that, the argument list for the function in the echo area. However, this documentation is only limited to a line or two of information. If you want the full document that Emacs' has for that function, class, or method, then Emacs gives you display-local-help, bound to C-h ..

(use-package eldoc
  :init
  (global-eldoc-mode))

/images/emacs-ide/eglot-eldoc-emacs.png

Other riffraff

Eldoc requires some configuration to work. However, imenu, xref, and completion-at-point don't require any configuration; they only have keybindings you need to learn.

Systems Keybindings Description
iMenu M-g i a system that uses completing-read for jumping to major definitions or sections of a file.
xref Is an ancient system that finds references and definitions for a major mode's identifiers.
M-. Jump to the definition of the symbol at point
M-, Jump back to the last location that invoke M-.
completion-at-point M-<TAB> Pops up possible completions for the symbol at point

Bindings

Eglot has many built-in functions, and I think some should be elevated to keybindings.

(use-package eglot
    ;;.
    :bind (:map
           eglot-mode-map
           ("C-c c a" . eglot-code-actions)
           ("C-c c o" . eglot-code-actions-organize-imports)
           ("C-c c r" . eglot-rename)
           ("C-c c f" . eglot-format)))

Criticisms

I think Emacs' built-in in-buffer completion system is still its weakest point. It lags behind all other major text editors, which provide completions as you type, and it provides those completions in a pop-up beside your cursor. Meanwhile, Emacs will only show you potential completions when you hit M-<TAB>, and it shows completions outside of your current one. This feels non-ergonomic, and the community agrees with me. There have been at least 3 pop-up completion frameworks for Emacs and I hope that one day soon Emacs core will settle on one.

A minor fix

Emacs doesn't come with a pop-up library. But we can use the magic of timers and advice to fix the autocomplete problem.

  (defvar complete-at-point--timer nil "Timer for triggering complete-at-point.")

  (defun auto-complete-at-point (&rest _)
    "Set a time to complete the current symbol at point in 0.1 seconds"
    (when (and (not (minibufferp)))
      (when (timerp complete-at-point--timer)
        (cancel-timer complete-at-point--timer))
      (setq complete-at-point--timer
            (run-at-time 0.1 nil-blank-string
                         (lambda ()
                           (when (timerp complete-at-point--timer)
                             (cancel-timer complete-at-point--timer))
                           (setq complete-at-point--timer nil)
                           (completion-at-point))))))

  (advice-add 'self-insert-command :after #'auto-complete-at-point)

Of course, if you only want completions to pop up at your behest, you can ignore the above code block and use M-<TAB> to your heart's content.

Completing our completing read

  (use-package eglot
    :hook (prog-mode . eglot-ensure)
    ;; The first 5 bindings aren't needed here, but are a good
    ;; reminder of what they are bound too
    :bind (("M-TAB" . completion-at-point)
           ("M-g i" . imenu)
           ("C-h ." . display-local-help)
           ("M-." . xref-find-definitions)
           ("M-," . xref-go-back)
           :map
           eglot-mode-map
           ("C-c c a" . eglot-code-actions)
           ("C-c c o" . eglot-code-actions-organize-imports)
           ("C-c c r" . eglot-rename)
           ("C-c c f" . eglot-format))
    :config
    (defvar complete-at-point--timer nil "Timer for triggering complete-at-point.")

    (defun auto-complete-at-point (&rest _)
      "Set a time to complete the current symbol at point in 0.1 seconds"
      (when (and (not (minibufferp)))
        ;; If a user inserts a character while a timer is active, reset
        ;; the current timer
        (when (timerp complete-at-point--timer)
          (cancel-timer complete-at-point--timer))
        (setq complete-at-point--timer
              (run-at-time 0.2 nil
                           (lambda ()
                             ;; Clear out the timer and run
                             ;; completion-at-point
                             (when (timerp complete-at-point--timer)
                               (cancel-timer complete-at-point--timer))
                             (setq complete-at-point--timer nil)
                             (completion-at-point))))))
    ;; Add a hook to enable auto-complete-at-point when eglot is enabled
    ;; this allows use to remove the hook on 'post-self-insert-hook if
    ;; eglot is disabled in the current buffer
    (add-hook 'eglot-managed-mode-hook (lambda ()
                                         (if eglot--managed-mode
                                             (add-hook 'post-self-insert-hook #'auto-complete-at-point nil t)
                                           (remove-hook 'post-self-insert-hook #'auto-complete-at-point t)))))
An example of pressing M-TAB and having a list of completions show up in an alternate buffer

Linting and Error-checking

Emacs has a built-in on-the-fly syntax checker called Flymake.

By default, Flymake supports ten languages, including Ruby. To get linting in Ruby, you will need to have Rubocop installed. Failing that, Flymake will use ruby -w -c. Like with ruby-ts-mode, we will use use-package to load and configure the package. We can tell Flymake to only start when ruby-ts-mode starts using :hook (ruby-ts-mode . flymake-mode). However, that means we'll have to add to this list each time we want to add Flymake to a new language. Instead, we could tell Flymake to add itself to the prog-mode-hook :hook (prog-mode . flymake-mode), thus ensuring that Flymake tries initializing itself in every programming-related buffer.

(use-package flymake
  :hook (prog-mode . flymake-mode))

Now, your buffers will light up a Christmas tree and yell at you for all your mistakes. Flymake comes with a couple of functions for understanding your errors and for navigating your mistakes.

  • flymake-goto-next-error

  • flymake-goto-prev-error

  • flymake-show-buffer-diagnostics

Unfortunately, none of these are bound to key chords. But we can fix that!

  (use-package flymake
    :hook (prog-mode . flymake-mode)
    ;; This first bind conflicts with eglot but is left here for
    ;; demonstrative purposes
    :bind (("C-h ." . display-local-help)
          :map flymake-mode-map
          ("C-c ! n" . flymake-goto-next-error)
          ("C-c ! p" . flymake-goto-prev-error)
          ("C-c ! l" . flymake-show-diagnostics-buffer)))
Emacs showing indicators in the fringes. The cursor is over an erroneous piece of code and has a diagnostic appearing in the minibuffer

Dealing with a bug in Eglot

When Eglot is enabled in a buffer, it controls the error diagnostic functionalities that Flymake normally handles. However, in my experience, Eglot has problems extracting diagnostics from the Ruby language server solargraph. Instead, I had to disable Eglot's integration with Flymake and rely on linters outside of the language servers.

(use-package eglot
  ;;...
  :init
  (setq eglot-stay-out-of '(flymake)))

Version Control

Like imenu and xref, Emacs' Version Control system, vc.el, is built-in and enabled by default. vc.el has been around for many years and has accumulated support for a bunch of version control systems.

For a system like git, you can use M-x vc-dir (C-x v d RET) to view the status of the current directory. If you're looking to diff things, Emacs gives you M-x vc-root-diff (C-x v D) to diff the entire repository or M-x vc-diff (C-x v =) to diff the current file.

To commit the changes for a file, you can use M-x vc-next-action (C-x v v), which will stage your current changes and prompt you to enter your commit message. Then, when you're done, you hit C-c C-c.

You don't need to add vc to your config file, but it may help to have some reminders for the keybindings

  (use-package vc
    ;; This is not needed, but it is left here as a reminder of some of the keybindings
    :bind (("C-x v d" vc-dir)
           ("C-x v =" vc-diff)
           ("C-x v D" vc-root-diff)
           ("C-x v v" vc-next-action))

Conflicting advice

I'd be remiss not to mention Emacs' two systems for dealing with merge conflicts. You have access to smerge, which stands for simple merge, that lets you put your cursor within the conflict and choose to keep the top, bottom, or both.

  (use-package smerge-mode
    :bind (:map smerge-mode-map
                ("C-c ^ u" . smerge-keep-upper)
                ("C-c ^ l" . smerge-keep-lower)
                ("C-c ^ n" . smerge-next)
                ("C-c ^ p" . smerge-previous)))

Or there is ediff, which is outside of the scope of this article to explain how to use.

This is only a tiny sampling of what vc.el can do, so I encourage you to read the docs and explore more.

A New Beginning

So, what have I accomplished?

If you already have an Emacs configuration but still want to try, you can save the code below in an init.el somewhere else on your hard drive and use --init-directory <folder containing init.el>~ to try it out. For instance, while writing this blog, I was saving my init file in /tmp/emacs/init.el and was running Emacs using emacs --init-dir /tmp/emacs

Let's look over our final config and see what we have.

  (use-package emacs
    :init
    (tool-bar-mode -1)
    (when scroll-bar-mode
      (scroll-bar-mode -1))
    (load-theme 'wombat)
    (set-face-attribute 'default nil :font "CaskaydiaCove Nerd Font Mono" :height 160)
    (fido-vertical-mode)
    :config
    (setq treesit-language-source-alist
          '((ruby "https://github.com/tree-sitter/tree-sitter-ruby"))))

  (use-package ruby-ts-mode
    :mode "\\.rb\\'"
    :mode "Rakefile\\'"
    :mode "Gemfile\\'"
    :hook (ruby-ts-mode . subword-mode)
    :bind (:map ruby-ts-mode-map
                ("C-c r b" . treesit-beginning-of-defun)
                ("C-c r e" . treesit-end-of-defun))
    :custom
    (ruby-indent-level 2)
    (ruby-indent-tabs-mode nil))

  (use-package eldoc
    :init
    (global-eldoc-mode))

  (use-package eglot
    :hook (prog-mode . eglot-ensure)
    :init
    (setq eglot-stay-out-of '(flymake))
    :bind (:map
           eglot-mode-map
           ("C-c c a" . eglot-code-actions)
           ("C-c c o" . eglot-code-actions-organize-imports)
           ("C-c c r" . eglot-rename)
           ("C-c c f" . eglot-format)))

  (use-package flymake
    :hook (prog-mode . flymake-mode)
    :bind (:map flymake-mode-map
                ("C-c ! n" . flymake-goto-next-error)
                ("C-c ! p" . flymake-goto-prev-error)
                ("C-c ! l" . flymake-show-buffer-diagnostics)))
  • โœ… Syntax Highlighting for programming language of choice

  • โœ… Display code diagnostics

  • โš ๏ธ Smart autocompletion

    • โŒ Autocomplete in buffer

    • โœ… Autocomplete in minibuffer

  • โš ๏ธ Make Emacs look pretty

  • โœ… Have support for version control

    • โœ… including built-in support for seeing changes made

Reflecting on this journey, Emacs 29 has come close to an authentic IDE experience. For instance, having to use M-TAB to generate a candidate list feels outdated, like a relic from the 90s. The default modeline also leaves much to be desired. It's cluttered, using obscure letters and ASCII symbols to display buffer information and listing every minor mode in use, which can be overwhelming.

Finding Emacs' extensive features and keybindings often resembles navigating a labyrinth. I only discovered help-at-point while writing this article.

However, the resilience and ingenuity of the Emacs community and its maintainers shine through these challenges. My auto-complete-at-point stands as a testament to the empowering nature of Emacs - if a feature is lacking or could be improved, the tools are there to craft it myself. This self-enhancement is something I advocate for. However, for the more substantial features, Emacs' package.el and package registries like Elpa, NonGnu Elpa, and Melpa are invaluable resources when looking for packages that fit my needs.

Emacs doesn't have all the toys I want included, but they've done a great job making it simple to configure Emacs to the point where I can be productive.

Bonus

Expanding to support to other languages

At the beginning of this post, I mentioned that it would be easy to extend support for other languages, and to prove my point, here is what I would do for JavaScript and TypeScript.

  ;; This package contains js-base-mode, js-mode, and js-ts-mode
  (use-package js-base-mode
    :defer 't
    :ensure js ;; I care about js-base-mode but it is locked behind the feature "js"
    :custom
    (js-indent-level 2)
    :config
    (add-to-list 'treesit-language-source-alist '(javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src"))
    (unbind-key "M-." js-base-mode-map))

  (use-package typescript-ts-mode
    :ensure typescript-ts-mode
    :defer 't
    :custom
    (typescript-indent-level 2)
    :config
    (add-to-list 'treesit-language-source-alist '(typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src"))
    (add-to-list 'treesit-language-source-alist '(tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src"))
    (unbind-key "M-." typescript-ts-base-mode-map))

External Packages

We can alleviate all of my major complaints by utilizing the packages on Elpa.

which-key helps you remember or discover key bindings by popping up suggestions of what to press next based on the last key chord you pressed.

  (use-package which-key
    :ensure t
    :commands (which-key-mode)
    :init
    (which-key-mode))

Instead of needing to write our own autocomplete framework, like auto-complete-at-point, we can rely on the stalwart company-mode.

  (use-package company
    :ensure t
    :commands (global-company-mode)
    :init
    (global-company-mode)
    :custom
    (company-tooltip-align-annotations 't)
    (company-minimum-prefix-length 1)
    (company-idle-delay 0.1))

Another level up, if eglot detects that markdown-mode is also installed, it will stylize docs generated by LSP servers

  (use-package markdown-mode
    :ensure t
    :magic "\\.md\\'")

And finally, we can cure my aesthetic woes by using nano-modeline to spruce up the place.

  (use-package nano-modeline
    :ensure t
    :init
    (nano-modeline-prog-mode t)
    :custom
    (nano-modeline-position 'nano-modeline-footer)
    :hook
    (prog-mode           . nano-modeline-prog-mode)
    (text-mode           . nano-modeline-text-mode)
    (org-mode            . nano-modeline-org-mode)
    (pdf-view-mode       . nano-modeline-pdf-mode)
    (mu4e-headers-mode   . nano-modeline-mu4e-headers-mode)
    (mu4e-view-mode      . nano-modeline-mu4e-message-mode)
    (elfeed-show-mode    . nano-modeline-elfeed-entry-mode)
    (elfeed-search-mode  . nano-modeline-elfeed-search-mode)
    (term-mode           . nano-modeline-term-mode)
    (xwidget-webkit-mode . nano-modeline-xwidget-mode)
    (messages-buffer-mode . nano-modeline-message-mode)
    (org-capture-mode    . nano-modeline-org-capture-mode)
    (org-agenda-mode     . nano-modeline-org-agenda-mode))
A much pettier Emacs with a more refined modeline bar and better in-buffer completions handle by something besides a shoddy function