Emacs has two mature LSP clients: Eglot, which ships with Emacs 29+ as a built-in package, and lsp-mode, a third-party package with a far larger feature surface. Choosing between them is not just a matter of features — it is a decision about how much configuration complexity you want to carry, what your workflow actually demands, and whether you are optimising for responsiveness or for capability.
This article compares both in detail, with specific guidance for PHP and Drupal development where both are viable choices.
The Core Philosophy Difference
Eglot's design goal, stated explicitly in its README, is to be a "minimal, opinionated" LSP client that leverages existing Emacs infrastructure rather than replacing it. Diagnostics go to Flymake. Completions go to completion-at-point. References appear in standard *xref* buffers. The entire package is around 4,000 lines of Elisp.
lsp-mode takes the opposite stance: it provides its own UI components (lsp-ui), its own debugger (dap-mode), integrates with every completion framework, and aims to replicate the full LSP feature set including experimental extensions. It supports all 150+ language servers in the LSP ecosystem with dedicated configuration profiles for each.
Neither philosophy is wrong — they serve different users.
Installation and Initial Setup
Eglot
Eglot is built into Emacs 29+. You only need configuration:
(use-package eglot
:ensure nil ; built-in, no need to install
:hook ((php-mode . eglot-ensure)
(web-mode . eglot-ensure)
(js-mode . eglot-ensure)
(typescript-mode . eglot-ensure))
:config
;; Add intelephense (PHP language server)
(add-to-list 'eglot-server-programs
'(php-mode . ("intelephense" "--stdio")))
;; Increase timeout for slow servers
(setq eglot-connect-timeout 30))
Install the PHP language server separately:
npm install -g intelephense
# or
npm install -g @phpactor/language-server
lsp-mode
(use-package lsp-mode
:ensure t
:hook ((php-mode . lsp-deferred)
(web-mode . lsp-deferred)
(js-mode . lsp-deferred)
(typescript-mode . lsp-deferred))
:init
(setq lsp-keymap-prefix "C-c l")
:config
(setq lsp-idle-delay 0.5
lsp-log-io nil))
(use-package lsp-ui
:ensure t
:commands lsp-ui-mode
:config
(setq lsp-ui-doc-enable t
lsp-ui-sideline-enable t
lsp-ui-peek-enable t))
lsp-mode can auto-install language servers with M-x lsp-install-server, which is a genuine quality-of-life advantage when working across many languages.
Feature Comparison
| Feature | Eglot | lsp-mode |
|---|---|---|
| Completions | Via completion-at-point (Corfu/Company) | Via completion-at-point + company-lsp |
| Diagnostics | Flymake (built-in) | Flymake or Flycheck |
| Hover docs | Eldoc (built-in tooltip) | lsp-ui floating window |
| Code actions | M-x eglot-code-actions | Sideline + modeline + command |
| Find references | xref (standard Emacs) | xref + lsp-ui-peek overlay |
| Rename symbol | M-x eglot-rename | lsp-rename |
| Inlay hints | Yes (Emacs 29+, via Eldoc) | Yes (lsp-inlay-hints) |
| Debugging | Not included | dap-mode (separate package) |
| Multiple servers per buffer | Yes (Emacs 29.1+) | Yes (lsp-multi-root) |
| Server auto-install | No | Yes (lsp-install-server) |
| Startup performance | Fast | Slower (more initialisation) |
| Configuration complexity | Low | High |
| Built-in to Emacs | Yes (Emacs 29+) | No |
Setting Up PHP Development with Eglot and Intelephense
For PHP/Drupal work, Intelephense is the recommended language server — it understands Drupal's coding conventions, provides accurate completions for hooks, and handles large codebases well.
;; Full Eglot + Intelephense setup for PHP/Drupal
(use-package eglot
:ensure nil
:hook (php-mode . eglot-ensure)
:config
;; Use Intelephense for PHP
(add-to-list 'eglot-server-programs
'(php-mode . ("intelephense" "--stdio")))
;; Intelephense settings passed as initializationOptions
(setq-default eglot-workspace-configuration
'((:intelephense
:stubs ["apache" "bcmath" "bz2" "calendar" "com_dotnet"
"Core" "ctype" "curl" "date" "dba" "dom" "enchant"
"exif" "FFI" "fileinfo" "filter" "fpm" "ftp" "gd"
"gettext" "gmp" "hash" "iconv" "imap" "intl" "json"
"ldap" "libxml" "mbstring" "meta" "mysqli" "oci8"
"odbc" "openssl" "pcntl" "pcre" "PDO" "pdo_ibm"
"pdo_mysql" "pdo_pgsql" "pdo_sqlite" "pgsql"
"Phar" "posix" "pspell" "random" "readline" "Reflection"
"session" "shmop" "SimpleXML" "snmp" "soap" "sockets"
"sodium" "SPL" "sqlite3" "standard" "superglobals"
"sysvmsg" "sysvsem" "sysvshm" "tidy" "tokenizer"
"xml" "xmlreader" "xmlrpc" "xmlwriter" "xsl" "Zend OPcache"
"zip" "zlib" "wordpress" "drupal"]
:telemetry (:enabled :json-false)
:diagnostics (:enable t)
:format (:enable t)))))
The stubs array tells Intelephense which API stubs to load. Adding "drupal" gives you completions for Drupal's global functions and services.
Essential Eglot Keybindings
(use-package eglot
:bind (:map eglot-mode-map
("C-c r" . eglot-rename)
("C-c C-a" . eglot-code-actions)
("C-c f" . eglot-format)
("C-c d" . eldoc)
("M-." . xref-find-definitions)
("M-," . xref-pop-marker-stack)
("M-?" . xref-find-references)))
Setting Up PHP Development with lsp-mode and Intelephense
(use-package lsp-mode
:ensure t
:hook (php-mode . lsp-deferred)
:init
(setq lsp-keymap-prefix "C-c l")
:config
;; Intelephense settings
(setq lsp-intelephense-stubs
["drupal" "wordpress" "apache" "bcmath" "Core" "date"
"dom" "fileinfo" "filter" "hash" "iconv" "json" "mbstring"
"mcrypt" "meta" "mysqli" "openssl" "pcre" "PDO" "Phar"
"posix" "readline" "Reflection" "session" "SimpleXML"
"soap" "sockets" "SPL" "sqlite3" "standard" "tokenizer"
"xml" "xmlreader" "xmlwriter" "zip" "zlib"]
lsp-intelephense-telemetry-enabled nil
lsp-idle-delay 0.3
lsp-log-io nil))
;; lsp-ui for floating docs and sideline hints
(use-package lsp-ui
:ensure t
:config
(setq lsp-ui-doc-enable t
lsp-ui-doc-position 'at-point
lsp-ui-sideline-show-diagnostics t
lsp-ui-sideline-show-code-actions t
lsp-ui-peek-always-show t))
Performance in Practice
On a typical Drupal project (several hundred PHP files), the performance difference between Eglot and lsp-mode is noticeable but not dramatic with modern hardware:
- Cold start: Eglot connects to the language server faster — it does less work on startup. lsp-mode initialises its UI components and workspace scanning in parallel, which can add 1-3 seconds on first open.
- Completion latency: Both are effectively the same — completion latency is dominated by the language server itself, not the client.
- Memory: lsp-mode's UI layer adds significant overhead. lsp-ui with sideline enabled continuously polls the server for diagnostics and code actions on every cursor movement.
- Native compilation: Both benefit enormously from Emacs native compilation (
--with-native-compilation). If you are still on interpreted Elisp, this matters more than client choice.
To profile Eglot performance:
;; Enable logging to diagnose slow responses
(setq eglot-events-buffer-size 2000000)
M-x eglot-events-buffer ; inspect the LSP message log
When to Choose Eglot
- You want zero external dependencies — Eglot is built in, nothing to install.
- You already use Corfu/Vertico/Flymake and want seamless integration with the modern Emacs completion stack.
- You value a clean, predictable configuration you can fully understand.
- You work primarily in 1-3 languages and do not need automatic server management.
- You care about startup time and memory footprint (important on slower machines or when running Emacs as a daemon for a long time).
When to Choose lsp-mode
- You want integrated debugging via dap-mode (Xdebug for PHP, node for JS).
- You work across many languages and want
M-x lsp-install-serverto handle server management. - You specifically want the lsp-ui floating documentation and sideline diagnostics UX — it genuinely looks closer to VS Code than Eglot's Eldoc approach.
- Your team or existing config is already on lsp-mode and conversion costs more than it saves.
The Debugger Question
This is where lsp-mode wins outright. If you need an integrated debugger for PHP (Xdebug) or JavaScript inside Emacs, dap-mode is the answer and it is part of the lsp-mode ecosystem:
(use-package dap-mode
:ensure t
:after lsp-mode
:config
(dap-auto-configure-mode)
(require 'dap-php))
Eglot has no equivalent. If you use Eglot and want debugging, your options are: switch to lsp-mode+dap-mode, use realgud for PHP/Xdebug (a separate Emacs debugger package), or debug via the terminal.
Summary: The Decision Tree
- Need integrated Xdebug/DAP debugging? → lsp-mode + dap-mode.
- Want floating UI docs and sideline diagnostics? → lsp-mode + lsp-ui.
- Want zero dependencies and a clean config? → Eglot.
- Already using Corfu + Vertico + Flymake? → Eglot integrates more naturally.
- Starting fresh on Emacs 29+? → Start with Eglot; switch to lsp-mode only if you hit a specific capability gap.
- Working across 10+ languages? → lsp-mode's auto-install is worth the overhead.
For most PHP/Drupal developers on Emacs 29+, Eglot with Intelephense is the right starting point. It is fast, well-integrated with the modern Emacs ecosystem, and covers 90% of what a language server is useful for: completions, diagnostics, go-to-definition, rename, and hover docs. Add lsp-mode only when you need what Eglot cannot provide.