Disclaimer: ChatGPT generated document.
The useful way to understand clangd is to first understand the larger idea it belongs to: language servers and the Language Server Protocol (LSP).
At a high level:
Your editor edits and displays text. A language server understands what that text means. LSP is the standardized conversation between them.
clangd is the language server from the LLVM/Clang ecosystem for C, C++, Objective-C, and related languages. It uses the actual Clang compiler infrastructure, which is a major reason it can understand complicated C++ accurately.
I'll build this from the ground up.
Imagine writing C++ in an editor.
You type:
std::vector<int> values;
values.You would like the editor to immediately suggest:
begin()
end()
size()
push_back()
...
You'd also like:
- syntax and semantic errors
- warnings
- autocomplete
- function signatures
- documentation on hover
- go to definition
- go to declaration
- find references
- rename symbol
- code actions
- refactoring
- semantic highlighting
- include insertion
- call hierarchy
- type hierarchy
- formatting
- inlay hints
These aren't really text-editor problems.
To answer:
"What does
valuesmean?"
something needs to understand C++.
It may have to know that:
std::vector<int> values;means:
values
↓
variable
↓
type std::vector<int>
↓
template std::vector<T>
↓
T = int
↓
members:
push_back
size
begin
end
...
That requires substantial compiler-like infrastructure.
Historically, IDEs tended to implement language intelligence themselves.
Conceptually:
Visual Studio
├── Editor
├── UI
├── C++ parser
├── C++ autocomplete
├── C++ symbol index
└── C++ refactoring
Eclipse
├── Editor
├── UI
├── C++ parser
├── C++ autocomplete
├── C++ symbol index
└── C++ refactoring
Another editor
├── Editor
├── UI
├── C++ parser
├── ...
This creates an ugly M × N problem.
Suppose there are:
10 editors
50 programming languages
In the worst case you potentially need something approaching:
10 × 50 = 500
editor/language integrations.
Furthermore, implementing excellent C++ intelligence is extraordinarily difficult.
You don't want every text editor implementing a C++ compiler frontend.
Instead, separate these responsibilities:
EDITOR
│
│ "What does this symbol mean?"
▼
┌───────────────┐
│ Language │
│ Server │
└───────────────┘
│
│ understands language
▼
parser / compiler /
type system / index
Now the editor doesn't need to understand C++.
It asks another program that does.
For C++:
Neovim
│
│ LSP
▼
clangd
│
▼
Clang
The editor handles things like:
drawing windows
cursor movement
keyboard input
menus
displaying diagnostics
completion popup
clangd handles things like:
What type is this expression?
Where is this function defined?
What members does this class have?
Is this code valid C++?
Where is this variable referenced?
This separation is enormously powerful.
Suppose every editor invented its own way to communicate with language servers.
You'd end up with:
clangd
/ | \
/ | \
Vim API | VS Code API
|
Emacs API
And every language server would need adapters for every editor.
We're back to the same problem.
So we need a standard protocol.
That is LSP.
The Language Server Protocol (LSP) defines standardized messages between:
Editor / IDE
↕
LSP
↕
Language Server
Microsoft introduced the protocol publicly in 2016, initially around VS Code's language tooling. Microsoft, Red Hat, and Codenvy announced broader adoption that June. The protocol grew partly from experience with TypeScript tooling and VS Code's language API. (Visual Studio Code)
The official specification is maintained openly:
Language Server Protocol specification
The current LSP site identifies 3.18 as the latest specification. (Microsoft GitHub)
The basic architecture becomes:
LSP
│
┌────────────┼────────────┐
│ │ │
clangd rust-analyzer gopls
│ │ │
C++ Rust Go
and:
VS Code
│
LSP
│
clangd
Neovim
│
LSP
│
clangd
Emacs
│
LSP
│
clangd
So the same server can support many editors. That's the central economic and architectural advantage of LSP. (Microsoft GitHub)
This terminology initially confuses a lot of people.
A language server usually isn't something running at:
https://some-server.com
It is commonly just a local process.
For example:
$ clangd
Your editor might spawn it roughly like:
Editor process
│
├── start process: clangd
│
├── stdin ──────────────►
│
└── stdout ◄────────────
The processes exchange structured messages.
No Internet connection is inherently required.
"Server" simply means:
a program providing a service to a client.
LSP uses JSON-RPC.
A simplified request might look conceptually like:
{
"jsonrpc": "2.0",
"id": 42,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": "file:///project/main.cpp"
},
"position": {
"line": 12,
"character": 8
}
}
}Meaning:
"The user is hovering over this position. Tell me what is there."
The server responds:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"contents": "std::vector<int>"
}
}The editor then decides how to render that information.
The LSP specification defines requests, responses and notifications over JSON-RPC, with messages conventionally framed using headers including Content-Length. (GitHub)
This distinction is useful.
A request expects an answer.
For example:
Editor:
"What is the definition of foo?"
Server:
"/src/foo.cpp:42"
A notification doesn't require one.
For example:
Editor:
"The user opened main.cpp."
Editor:
"The user changed line 57."
Editor:
"The user closed main.cpp."
Typical LSP methods therefore include concepts such as:
textDocument/didOpen
textDocument/didChange
textDocument/didClose
textDocument/hover
textDocument/completion
textDocument/definition
textDocument/references
textDocument/rename
The official specification separates lifecycle, document synchronization, language, workspace, window and miscellaneous functionality. (Microsoft GitHub)
An LSP session looks roughly like this.
Editor
│
│ launch clangd
▼
clangd
Then:
Editor ── initialize ──► clangd
Editor ◄─ capabilities ─ clangd
The two sides negotiate capabilities.
The editor might support:
completion
semantic tokens
snippets
code actions
inlay hints
The server announces what it supports too.
This capability negotiation lets LSP evolve without requiring every client and server to implement every feature simultaneously. (GitHub)
Suppose you open:
main.cpp
The editor sends something equivalent to:
textDocument/didOpen
including the document contents.
Why send the contents when clangd could read the file?
Because your editor may contain:
what's on disk:
int x = 10;
what's currently in editor:
int x = 20;
The second version hasn't necessarily been saved.
The language server needs to analyze the code you're actually editing, not merely the last saved version.
So it maintains an in-memory representation of open documents.
Imagine:
foo.babecomes:
foo.barThe editor sends a change notification.
Conceptually:
didChange:
position = ...
text = "r"
clangd updates its view of the file.
Then it can recompute relevant syntax/semantic information.
You type:
player.The editor sends:
textDocument/completion
clangd determines the type of player.
Maybe:
Player player;Then it investigates the members of:
class Player {
public:
void jump();
void shoot();
int health();
};and responds with completion items.
The editor displays:
jump()
shoot()
health()
Notice the separation:
clangd:
determines WHICH completions make sense
editor:
determines HOW completions look
That's fundamental to LSP.
Suppose you write:
int x = "hello";clangd analyzes it and determines there's a type error.
It sends diagnostics to the client.
Conceptually:
clangd
│
│ diagnostic:
│ cannot initialize int with const char*
▼
Editor
The editor decides whether to render that as:
red squiggly line
gutter icon
Problems window
popup
Again:
semantic knowledge → language server
presentation → editor
clangd is the LLVM project's C-family language server.
Its important characteristic is:
clangd is built on Clang.
Clang is the C-family compiler frontend from LLVM.
Roughly:
LLVM ecosystem
LLVM
│
├── LLVM IR
├── optimizers
├── code generators
│
└── Clang
│
├── lexer
├── preprocessor
├── parser
├── AST
├── type checking
├── diagnostics
└── clangd
This is hugely important.
clangd isn't merely doing:
regex searches
or guessing from text.
It has access to real compiler infrastructure.
The clangd FAQ notes, for example, that clangd uses the Clang parser and defines normal Clang preprocessor symbols such as __clang__. (Clangd)
clangd emerged from the LLVM/Clang ecosystem after LSP had demonstrated the value of separating language intelligence from editors.
The clangd project describes clangd 7 as its first usable release. LLVM 7 dates from 2018, so that is a useful landmark for clangd becoming practical. (Clangd)
Since then it has followed LLVM's release cycle. The project states that LLVM release branches are generally created twice yearly, with stable releases roughly around March and September. (Clangd)
As of August 2026, clangd's release feed includes the LLVM/clangd 22.x line, alongside snapshot builds. (GitHub)
Consider:
int square(int x) {
return x * x;
}Clang doesn't fundamentally see this as a string.
It builds an Abstract Syntax Tree (AST).
Very approximately:
FunctionDecl
├── name: square
├── return type: int
├── ParmVarDecl
│ ├── name: x
│ └── type: int
└── CompoundStmt
└── ReturnStmt
└── BinaryOperator '*'
├── DeclRefExpr x
└── DeclRefExpr x
Now consider:
square(5)The AST can connect that expression with:
FunctionDecl square
That makes things like:
Go to definition
Find references
Rename
Hover
much more principled.
C++ tooling is unusually hard because meaning depends heavily on compilation context.
Consider:
#ifdef WINDOWS
using Handle = void*;
#else
using Handle = int;
#endifWhat is:
Handle?
It depends on compiler flags.
Or:
#include <some_library.hpp>Where is that header?
Depends on:
-I flags
system include directories
compiler
sysroot
SDK
target platform
Or:
#ifdef FEATURE_XDepends on:
-DFEATURE_X
So clangd cannot correctly understand a serious C++ project merely by seeing its source files.
It needs to know:
How would this file actually be compiled?
This leads to one of the most important clangd concepts.
clangd works best when it has a compilation database.
Usually:
compile_commands.json
Example:
[
{
"directory": "/home/me/project/build",
"command": "clang++ -I../include -std=c++23 -DFOO=1 -c ../src/main.cpp",
"file": "../src/main.cpp"
}
]This tells clangd:
main.cpp is compiled using:
clang++
-I../include
-std=c++23
-DFOO=1
...
Then clangd can reproduce essentially the same parsing environment as your compiler.
This is why clangd's getting-started instructions boil the requirements down to three things: install clangd, connect it to an editor, and tell clangd how the project is built. (Clangd)
For CMake projects you commonly generate the database with:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ...which creates:
build/
└── compile_commands.json
Then clangd can use it.
Other build systems have analogous mechanisms or tools that generate compilation databases.
A useful simplified architecture is:
clangd
│
┌─────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Compilation Parser Background
Database Index
│ │ │
▼ ▼ ▼
compiler flags AST project symbols
│
▼
headers/macros/etc
There are essentially two big worlds.
clangd parses the file you're actively editing.
This gives extremely detailed information:
AST
types
expressions
overloads
templates
diagnostics
local variables
clangd also builds an index of symbols across the project.
This helps answer:
Where is Foo defined?
Who references bar()?
What classes derive from Base?
without reparsing the entire project for every keystroke.
Suppose your project contains:
10,000 source files
3 million lines of C++
When you request:
Find references to Database::connect
clangd can't realistically parse every file from scratch.
Instead it maintains an index.
Conceptually:
Symbol index
Database::connect
├── src/database.cpp:43
├── src/server.cpp:81
├── src/client.cpp:123
├── tests/database_test.cpp:55
└── ...
This is conceptually similar to a search index.
You pay indexing cost earlier so later queries are fast.
clangd can crawl the compilation database and index the project in the background.
Imagine:
compile_commands.json
│
▼
Background Index
│
┌─────────────┼─────────────┐
▼ ▼ ▼
a.cpp b.cpp c.cpp
│ │ │
└─────────────┼─────────────┘
▼
Symbol Index
Then interactive queries can consult that index.
C++ has another notorious problem:
#include <vector>
#include <string>
#include <unordered_map>
#include <boost/...>
#include "massive_project_header.hpp"Parsing all those headers after every keystroke would be terrible.
Compiler infrastructure therefore makes heavy use of reusable parsed state.
clangd maintains a preamble for the relatively stable beginning of a translation unit.
Very approximately:
main.cpp
#include ...
#include ...
#define ...
│
│ relatively stable
▼
PREAMBLE
────────────────
int main() {
...
}
▲
│ changes frequently
The expensive header-heavy part can be reused while the body you're actively changing is reparsed.
This is one of the tricks that makes interactive compiler-grade C++ analysis feasible.
Because it uses Clang, clangd can understand things such as:
template<typename T>
concept Numeric = std::integral<T>;
template<Numeric T>
T add(T a, T b) {
return a + b;
}including:
templates
concepts
overload resolution
namespaces
macros
preprocessor state
inheritance
type deduction
auto
decltype
operator overloads
C++ standard library types
This is far beyond simple textual analysis.
Suppose:
auto result = calculate();You hover over:
result
clangd may know that:
result: std::optional<Result>
even though the source literally says:
autoThat's semantic analysis.
Suppose:
foo.calculate();There could be 100 functions named:
calculatein the project.
Text search can't reliably determine which one is called.
clangd performs C++ semantic analysis:
foo
↓
type Foo
↓
candidate functions
↓
overload resolution
↓
Foo::calculate(int)
↓
definition location
Then it returns that location through LSP.
Rename looks deceptively simple.
You might think:
replace every occurrence of "foo"
But:
void foo();
class A {
int foo;
};
namespace B {
void foo();
}These are completely different symbols.
A language-aware rename changes references to the specific declaration, not arbitrary matching text.
That's why semantic tooling matters.
Suppose:
std::unique_ptr<Player> p;
p->clangd needs to reason through:
p
↓
std::unique_ptr<Player>
↓
operator->
↓
Player*
↓
Player
↓
members
Then rank useful candidates.
Modern autocomplete is essentially a specialized interactive compiler query.
This is extremely important.
LSP says approximately:
Here's how an editor asks for completion.
It does not say:
Here's how you compute completion.
A server could implement completion using:
regex
parser
compiler
database
machine learning
static analysis
external API
LSP doesn't care.
It standardizes the interface, not the intelligence.
If you understand HTTP APIs, a useful analogy is:
HTTP API:
client:
GET /users/42
server:
{ "name": "Alice" }
LSP:
client:
textDocument/definition(position)
server:
file:///foo.cpp:42
The important difference is that LSP sessions are generally stateful and bidirectional.
The server knows which documents are open and changing, and the server can send information to the editor too.
People often blur these terms.
More precisely:
┌──────────────────────────┐
│ Editor │
│ │
│ ┌──────────────────┐ │
│ │ LSP Client │ │
│ └────────┬─────────┘ │
└────────────┼─────────────┘
│
│ JSON-RPC / LSP
│
┌────────────▼─────────────┐
│ Language Server │
│ clangd │
└──────────────────────────┘
For example, Neovim contains an LSP client.
clangd is the server.
Historically, Vim language plugins often implemented their own mechanisms.
Modern Neovim has built-in LSP client support.
So:
Neovim
│
├── clangd → C/C++
├── rust-analyzer → Rust
├── gopls → Go
├── pyright/etc. → Python
└── lua-language-server
The editor implements the general LSP machinery once.
Language-specific intelligence lives elsewhere.
Today the architecture appears across a broad range of development environments.
Editors and IDEs can include things such as:
VS Code
Visual Studio
Neovim
Vim
Emacs
Eclipse-based environments
Helix
Kate
Sublime Text
various browser/cloud editors
Microsoft's own Visual Studio documentation describes LSP as a way to implement one language service that can supply features such as IntelliSense, diagnostics and references to different editors. (Microsoft Learn)
Different clients support different portions and extensions of LSP, so "supports LSP" doesn't necessarily mean every feature behaves identically.
A few useful examples:
C / C++ clangd
Rust rust-analyzer
Go gopls
Lua lua-language-server
Java Eclipse JDT Language Server
Haskell Haskell Language Server
Python Pyright/Pylance and others
C# several .NET/C# language-service approaches
Some servers were explicitly built around LSP.
Others expose an existing compiler/type-checker/tooling engine through an LSP layer.
This distinction matters.
A compiler normally does:
source
↓
parse
↓
semantic analysis
↓
optimization
↓
machine code / bytecode
A language server does something closer to:
source
↓
parse / analyze / index
↓
answer interactive questions
Such as:
What's here?
Is this wrong?
Where is this defined?
What could I type next?
What references this?
Can this be renamed?
clangd shares compiler infrastructure with Clang, but its product is editor intelligence rather than an executable.
Imagine running:
clang++ main.cppafter every keystroke.
That would be:
slow
wasteful
poorly suited to incomplete code
not structured around editor queries
A language server stays alive.
It caches:
ASTs
indexes
headers
preambles
symbol information
project state
So instead of:
start compiler
parse everything
answer
exit
for every request, you get:
start clangd
parse/cache/index
request
request
request
edit
request
edit
request
...
That's a huge architectural difference.
Compilers mostly expect programs heading toward valid code.
Editors constantly see nonsense:
std::vector<or:
foo.or:
if (A language server must remain useful while the program is temporarily invalid.
That's one reason language tooling isn't just:
"run compiler and print errors."
It needs error recovery and partial understanding.
Interactive editing creates competing work:
user types
↓
parse requested
↓
user types again
↓
old parse already obsolete
Good language servers therefore need sophisticated scheduling and cancellation.
For example:
Request #100 completion
│
├── expensive analysis...
│
Request #101 arrives
│
└── #100 may no longer matter
LSP supports cancellation mechanisms, and servers can structure their internals to prioritize interactive work over less urgent indexing.
For a compiler:
300 ms
may be excellent.
For autocomplete:
300 ms
can feel sluggish.
Language servers therefore care deeply about:
caching
incremental computation
background indexing
prioritization
cancellation
precomputed indexes
reuse of parsed state
The entire architecture is optimized around an interactive human waiting for feedback.
Before LSP:
IDE = editor + language intelligence
After LSP, we can think:
Editor
+
standard protocol
+
language intelligence
This allowed lightweight editors to gain IDE-class capabilities without implementing compilers themselves.
It also allowed language developers to concentrate effort on one excellent language engine.
clangd represents an especially clean version of the architecture:
clangd
│
┌────────────┴────────────┐
│ │
Compiler-grade LSP interface
understanding │
│ │
Clang Any LSP editor
The same machinery that understands C++ for compilation can be reused for development tooling.
That reduces semantic disagreements like:
compiler says one thing
IDE parser says another
though tooling and actual builds can still disagree when they're configured differently.
Probably the single most useful debugging lesson is this:
When clangd misunderstands C++, first suspect the compile command.
Suppose your actual build uses:
clang++ \
-std=c++23 \
-I/opt/library/include \
-DMY_FEATURE=1 \
main.cppbut clangd thinks it's:
clang++ main.cppThen it may report:
header not found
unknown identifier
unsupported language feature
wrong #ifdef branch
incorrect type
clangd isn't necessarily broken.
It is effectively compiling a different program.
When debugging clangd, ask:
What command does clangd think compiles this file?
Then compare it to the real build.
Check:
compiler
working directory
-I paths
-isystem paths
-D macros
-std=
target
sysroot
generated headers
toolchain
This solves a surprisingly large fraction of problems.
clangd also supports project configuration through files such as:
.clangd
You can use configuration to modify compile flags and behavior.
For example, the clangd documentation shows configuration patterns that add/remove compilation flags. (Clangd)
Conceptually:
CompileFlags:
Add:
- -WallBut generally the best foundation is still:
accurate build system
↓
accurate compile_commands.json
↓
clangd
rather than manually recreating your entire build configuration.
You'll sometimes encounter another protocol:
DAP — Debug Adapter Protocol.
They're related philosophically but solve different problems.
LSP
Editor ↔ language intelligence
DAP
Editor ↔ debugger
So:
clangd
can provide:
completion
diagnostics
navigation
refactoring
while a debugger such as:
GDB
LLDB
provides:
breakpoints
step
continue
inspect variables
stack traces
A debug adapter can standardize the editor/debugger relationship in much the same spirit as LSP standardized language tooling.
Another subtle distinction.
Basic highlighting can be purely lexical:
int main() {The editor can recognize:
int → keyword/type
main → identifier
with something like a grammar.
But consider:
Foo bar;Is Foo:
class?
typedef?
template?
macro?
namespace?
Semantic highlighting requires actual program understanding.
Modern LSP supports semantic tokens, allowing servers to provide richer semantic classification.
LSP 3.17, for example, added major features including type hierarchy, inlay hints, inline values and notebook-document support, illustrating how the protocol has expanded beyond its original core. (GitHub)
This is an important limitation.
Two editors using the same clangd can still feel different because the client controls:
completion UI
snippet support
when requests fire
diagnostic presentation
code action UI
semantic highlighting
configuration
keybindings
feature support
So:
Neovim + clangd
and:
VS Code + clangd
share much of the underlying semantic intelligence but not necessarily the experience.
Real servers sometimes implement additional methods not standardized by LSP.
Conceptually:
Standard LSP:
textDocument/hover
clangd-specific:
clangd/someFeature
Clients can choose whether to support them.
This gives an interesting tradeoff:
standardization
↕
innovation
If everything must be standardized first, experimentation becomes slow.
If everyone invents extensions, portability declines.
The ecosystem continuously balances those concerns.
Research has even proposed standardized LSP extensions for domains such as specification languages because specialized tools often need capabilities beyond mainstream programming-language LSP. (arXiv)
Think of clangd as a long-running, interactive Clang frontend with a project index and an LSP interface.
That's not a complete implementation description, but it's an excellent mental model:
┌───────────────┐
│ Editor │
└───────┬───────┘
│
LSP
│
┌───────▼───────┐
│ clangd │
└───────┬───────┘
│
┌───────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
Clang AST Symbol Index Compilation DB
│ │ │
▼ ▼ ▼
current code whole project build flags
Suppose you have:
// player.hpp
class Player {
public:
void jump();
};and:
// main.cpp
#include "player.hpp"
int main() {
Player p;
p.j
}You type:
j
Here's approximately what happens.
1. Keyboard
│
▼
2. Editor updates its text buffer
│
▼
3. Editor sends didChange
│
▼
4. clangd updates main.cpp
│
▼
5. Editor requests completion
│
▼
6. clangd parses/analyzes current code
│
▼
7. Clang determines:
p has type Player
│
▼
8. clangd examines Player members
│
▼
9. candidate:
jump()
│
▼
10. clangd sends CompletionItem
│
▼
11. Editor receives it
│
▼
12. Completion popup appears:
jump()
That's the whole ecosystem in miniature.
The flow becomes:
Editor
│
│ textDocument/definition
│ position = p.jump()
▼
clangd
│
│ semantic lookup/index
▼
player.cpp:17
│
▼
Editor
│
▼
opens player.cpp at line 17
clangd doesn't open the editor window.
It returns a location.
The client handles navigation.
Editor:
rename Player → Character
clangd:
determine actual symbol
find semantic references
calculate edits
clangd → editor:
edit these locations
editor:
apply edits
This illustrates another LSP principle:
The language server often proposes structured operations; the client performs the user-facing action.
It gives three groups strong advantages.
For language implementers:
build language intelligence once
↓
VS Code
Neovim
Emacs
Eclipse
other clients
For editor developers:
implement LSP client once
↓
C++
Rust
Go
Python
Lua
Java
...
For users:
choose editor independently
from language tooling
That last point has had enormous practical consequences.
You no longer necessarily need to choose an IDE primarily because it owns the best parser for your language.
LSP isn't magic.
It doesn't standardize:
build systems
debuggers
test runners
package managers
version control
terminal integration
project generation
deployment
And it cannot make a poor language engine good.
If a server has:
bad parser
bad type checker
bad indexing
slow implementation
then putting LSP in front of it doesn't fix that.
LSP standardizes communication.
For many languages:
foo.py
can be analyzed reasonably well just from the file and project environment.
C++ is more context-sensitive.
A file may have entirely different meaning under:
g++ -std=c++17 ...versus:
clang++ -std=c++23 -DMODE=SERVER ...So this relationship is fundamental:
build system
│
▼
compile_commands.json
│
▼
clangd
│
▼
accurate AST
│
▼
accurate editor features
If you remember only one clangd-specific architectural fact, remember that.
clangd provides the language intelligence engine, not the complete IDE.
A full environment may consist of:
Neovim
├── text editing
├── UI
├── LSP client
│ └── clangd
├── DAP client
│ └── LLDB adapter
├── Tree-sitter
├── Git integration
├── CMake tooling
└── terminal
So people sometimes say:
"clangd is my C++ IDE."
More accurately:
clangd provides a large part of the semantic backend that makes their editor behave like a C++ IDE.
This is another useful comparison.
Tree-sitter is primarily designed for fast incremental syntactic parsing.
clangd performs compiler-grade semantic analysis.
Roughly:
Tree-sitter:
"What syntactic structure is this?"
clangd:
"What does this program mean?"
For:
foo.bar()Tree-sitter can identify something like:
call_expression
member_expression
identifier
clangd can potentially determine:
foo is Foo&
bar resolves to Foo::bar(int)
defined at foo.cpp:82
returns std::string
may be deprecated
Editors often use both because they solve different problems.
These terms are worth separating:
Lexer
characters → tokens
Parser
tokens → syntax tree
Semantic analyzer
syntax tree → meaning/types/symbols
Indexer
symbols/references → searchable database
Compiler
source → executable/object/etc.
Language server
editor requests → language-aware answers
clangd combines or reuses several of these components.
Good language servers need much more than correctness.
They need:
Correctness
understand language accurately
Speed
autocomplete should feel instantaneous
Incrementality
don't recompute the universe after every keystroke
Error tolerance
understand incomplete/broken code
Scalability
work on million-line repositories
Memory efficiency
indexes and ASTs can become enormous
Concurrency
index while responding to interactive queries
Cancellation
stop work that's already obsolete
Stable protocol behavior
interoperate with many clients
clangd is interesting precisely because all of these requirements meet one of the world's most complicated mainstream programming languages.
LSP is more important than "autocomplete over JSON."
It changed the architecture of programming tools.
Before:
┌─────────────────────────┐
│ IDE │
│ │
│ editor │
│ compiler-ish parser │
│ completion engine │
│ refactoring engine │
│ indexing engine │
└─────────────────────────┘
After:
┌──────────────┐
│ Editor │
└──────┬───────┘
│
LSP
│
┌──────▼───────┐
│ Language │
│ Intelligence│
└──────────────┘
The same idea has subsequently influenced other standardized tooling protocols and service-oriented developer tooling.
Standard protocols introduce a lowest-common-denominator problem.
Imagine language X has an amazing concept:
"show proof obligations"
but LSP doesn't define such a request.
The language server has three choices:
1. Don't expose it.
2. Abuse an existing LSP concept.
3. Add a custom extension.
Option 3 means clients need special support.
So the central tension is:
portability
vs
language-specific power
LSP continues evolving partly to absorb features that prove broadly useful.
Even though everything may run locally, you now have:
Editor process
↕
protocol
↕
Language server process
That introduces problems reminiscent of distributed systems:
messages arrive asynchronously
requests become stale
process can crash
versions differ
capabilities differ
state must stay synchronized
requests need cancellation
The architecture is cleaner organizationally but not necessarily simpler internally.
When something fails, there are several possible layers:
Problem
│
┌────────────┼─────────────┐
▼ ▼ ▼
Editor LSP client Server
│
▼
clangd
│
▼
project configuration
│
▼
build system
For clangd specifically, I'd investigate roughly:
1. Is clangd running?
2. Is the editor actually connected?
3. Is the correct project root detected?
4. Is compile_commands.json found?
5. Is the correct compile command being used?
6. Are generated headers present?
7. Are include paths correct?
8. Are compiler/target/sysroot settings correct?
9. Is the clangd version reasonably current?
10. Only then suspect a clangd bug.
Instead of just configuring an editor, experiment with the architecture.
Start clangd with verbose logging:
clangd --log=verboseThen use your editor and watch what happens.
You'll begin seeing:
file opened
compile command selected
AST built
index activity
completion requests
diagnostics
It transforms clangd from "editor magic" into an understandable program.
The official project documentation is the best next reference:
And for the protocol itself:
Here's the model I'd keep in your head:
YOU
│
│ keyboard
▼
┌─────────────────┐
│ EDITOR │
│ │
│ Neovim/VS Code │
│ Emacs/etc. │
└────────┬────────┘
│
LSP CLIENT
│
│ JSON-RPC
│
┌────────▼────────┐
│ clangd │
│ │
│ language server │
└────────┬────────┘
│
┌───────────┼────────────┐
│ │ │
▼ ▼ ▼
Clang Project Compile
AST Index Commands
│ │ │
│ │ ▼
│ │ build system
│ │
└──────┬────┘
│
▼
semantic knowledge
│
│
┌──────┴──────┐
▼ ▼
"foo is an int" "bar() is
defined at
foo.cpp:73"
│
▼
LSP
│
▼
EDITOR
│
▼
completion / hover /
diagnostics / navigation
If everything above fades away, keep these three.
First: a language server separates language intelligence from the editor.
Editor = presentation/editing
clangd = C++ understanding
Second: LSP standardizes their conversation.
Editor
↕
JSON-RPC / LSP
↕
Language server
That lets many editors reuse many language servers.
Third: clangd is essentially compiler-grade C++ intelligence turned into an interactive service.
Clang parser + AST + semantic analysis
+
project-wide index
+
compilation database
+
LSP
=
clangd
And the particularly important C++ insight is:
C++ source
+
compile flags
+
headers/macros/toolchain
=
actual program clangd must understand
That's why compile_commands.json matters so much.
Once you understand those ideas, features like autocomplete, hover, rename, references and diagnostics stop looking like IDE magic. They're simply structured semantic questions sent to a long-running compiler-like program that has been optimized to answer them interactively.
How Modern IDEs Use Language Servers and Debuggers: CLion, Visual Studio, LSP, DAP, and Native Engines
Yes. The answer is basically “it depends on the IDE, and often they use a hybrid.” Full IDEs like CLion and Visual Studio have enough internal infrastructure that they don't have to structure themselves as “generic LSP client + external server” in the same way Neovim or VS Code often does.
For CLion, the current situation is particularly interesting. CLion has two C/C++ language engines: a clangd-based engine and JetBrains' own C++ engine, derived from the ReSharper C++/Rider technology. In current CLion Nova, JetBrains says the ReSharper-based engine handles the core IDE features, while clangd is still used alongside it for parts of the C++ experience. Older/Classic CLion leaned more directly on clangd for things such as completion, highlighting, diagnostics, navigation, and quick fixes. (JetBrains)
So CLion is roughly:
CLion
│
┌────────────┼────────────┐
│ │
▼ ▼
JetBrains C++ engine clangd-based engine
(ReSharper C++ / Nova) (Clang)
│ │
└───────────┬─────────────┘
▼
IDE features
The important subtlety is that “uses clangd” does not necessarily mean “talks to stock clangd exactly like Neovim does over ordinary LSP.” An IDE vendor can embed, modify, wrap, or communicate with compiler infrastructure using tighter/private interfaces. JetBrains describes it as a clangd-based engine integrated into CLion, rather than simply positioning CLion as a generic LSP frontend. (JetBrains)
CLion's debugger story is similar but easier to see. It natively integrates GDB and LLDB, and, since more recent releases, it can also talk to third-party debuggers using DAP. Current CLion documentation lists LLDB, GDB, an LLDB-based MSVC debugger, and debuggers supporting the Debug Adapter Protocol. (JetBrains)
So for debugging:
CLion
│
┌───────────┼────────────┐
│ │ │
▼ ▼ ▼
GDB LLDB DAP debugger
│ │ │
native/tight integration │
│
standardized DAP
That means DAP is supported, but CLion doesn't have to force every debugging backend through DAP.
For Microsoft Visual Studio, C++ is much more of a traditional integrated-IDE architecture.
Visual Studio's normal C++ IntelliSense is Microsoft's own C++ language service, not clangd. Microsoft documents its own C++ IntelliSense system for completion, browsing, references, refactoring, diagnostics, navigation, and so forth. Historically, parts of its semantic frontend have been based on an EDG compiler frontend rather than Clang. (Microsoft Learn)
So when you write C++ in normal Visual Studio:
Visual Studio
│
▼
Microsoft C++ language service
│
├── IntelliSense
├── navigation
├── refactoring
├── indexing
└── diagnostics
rather than:
Visual Studio
│
LSP
│
clangd
A particularly useful distinction is that Visual Studio can compile your project with Clang without using clangd for IntelliSense.
Microsoft explicitly supports Clang/LLVM as a compiler/toolchain. (Microsoft Learn)
You could therefore have:
Visual Studio
│
┌──────┴──────┐
▼ ▼
C++ IntelliSense Build
│ │
Microsoft engine clang-cl
│
▼
LLVM/Clang
This surprises people initially because they assume:
“I'm compiling with Clang, therefore autocomplete must be clangd.”
No. Compiler selection and language-server selection are independent architectural choices.
The same is true in CLion:
compiler:
GCC / Clang / MSVC / ...
language intelligence:
JetBrains engine + clangd-based components
Those don't have to be the same program.
Visual Studio does support LSP as an extensibility mechanism, though. Microsoft exposes infrastructure allowing language extensions to implement a language server and plug it into Visual Studio. But that doesn't mean Microsoft's built-in C++ support is itself implemented as “clangd through LSP.” (Microsoft Learn)
Think of it as:
Visual Studio
├── built-in language services
│ ├── C++
│ ├── C#
│ └── ...
│
└── LSP infrastructure
│
└── external/new language servers
That's a very common architecture for heavyweight IDEs.
The same distinction exists for debugging.
Visual Studio has had its own debugger architecture for decades, much older than DAP. For Windows-native C++, it doesn't need to put DAP between its own UI and debugger just to conform to a standard it controls neither historically nor architecturally.
Conceptually:
Visual Studio
│
VS debugger engine
│
┌───────────┼───────────┐
▼ ▼ ▼
native managed other
Windows .NET targets
For remote Linux C++, Visual Studio can launch GDB on the remote machine and connect that to Visual Studio's own debugging frontend. Microsoft's documentation explicitly describes Visual Studio launching GDB remotely while presenting the Visual Studio debugger experience. ([Microsoft Learn](https://learn.microsoft.com/fi-fi/ cpp/linux/download-install-and-setup-the-linux-development-workload?view=msvc-180&utm_source=chatgpt.com))
That's a great example of something that accomplishes the same architectural goal as DAP without necessarily using DAP:
Visual Studio UI
│
Microsoft debugger integration
│
GDB
│
Linux process
versus the standardized model:
Editor
│
DAP
│
Debug adapter
│
GDB
Because LSP is primarily valuable at an organizational boundary.
If you're building Neovim and don't want to implement C++, Rust, Go, Java, Python, etc., LSP is fantastic:
Neovim
│
LSP
├──── clangd
├──── rust-analyzer
├──── gopls
└──── ...
But imagine JetBrains owns both:
CLion frontend
and
ReSharper C++ engine
They can define arbitrary APIs between them.
For example, they could have an internal function conceptually like:
SemanticModel resolveExpression(
DocumentId document,
TextRange range,
AnalysisOptions options,
CachedProjectModel& project);Instead of serializing everything to:
{
"jsonrpc": "2.0",
"method": "textDocument/hover",
...
}If both halves are controlled by the same company, a private API can be:
- richer
- faster
- more tightly integrated
- easier to extend without standardization
- capable of exposing IDE-specific concepts
- capable of sharing memory or object models directly
LSP's strength is interoperability, not necessarily maximum integration.
So you can think of two extremes:
Loose integration Tight integration
Editor ── LSP ── server IDE ── private API ── engine
portable vendor-specific
easy to swap deeply integrated
standardized arbitrary capabilities
process boundary common may be in-process/out-of-process
Neither is inherently “better.”
And this gives you a nice way to classify several environments.
Neovim / Helix / Emacs-style setups tend strongly toward:
Editor
├── LSP client
│ ├── clangd
│ ├── rust-analyzer
│ └── ...
│
└── DAP client
├── codelldb
├── debugpy
└── ...
The protocols are doing a lot of architectural work.
VS Code is somewhat similar, although extensions can use APIs beyond pure LSP:
VS Code
│
Extension
│
LSP
│
server
This architecture is one of the main historical reasons LSP became so successful.
Visual Studio sits farther toward:
Large integrated IDE
│
├── proprietary/native language services
├── proprietary debugger infrastructure
│
└── LSP available where useful
And CLion currently lives somewhere in between:
CLion
│
┌──────────────┼──────────────┐
▼ ▼ ▼
JetBrains C++ clangd-based debugger
engine engine │
├─ GDB
├─ LLDB
└─ DAP
JetBrains' current documentation explicitly calls out the two parallel C++ engines, and CLion Nova has increasingly shifted core semantic functionality to JetBrains' own ReSharper-derived engine. (JetBrains)
One especially interesting consequence is that the three things are completely separable:
┌─────────────────────┐
│ Language semantics │
│ clangd / IntelliSense│
└─────────────────────┘
┌─────────────────────┐
│ Compiler │
│ GCC / Clang / MSVC │
└─────────────────────┘
┌─────────────────────┐
│ Debugger │
│ GDB / LLDB / VS dbg │
└─────────────────────┘
For example, a perfectly sensible setup can be:
Editor: CLion
Code engine: JetBrains + clangd components
Compiler: GCC
Debugger: GDB
Build: CMake/Ninja
or:
Editor: Visual Studio
Code engine: Microsoft IntelliSense
Compiler: clang-cl
Debugger: Visual Studio debugger
Build: MSBuild
or:
Editor: Neovim
Code engine: clangd over LSP
Compiler: GCC
Debugger: LLDB via a DAP adapter
Build: CMake/Ninja
That separation is one of the most useful things to internalize. LSP isn't the language engine, DAP isn't the debugger, and clangd isn't the compiler. They are different layers that modern tools can mix and match.
