Skip to content

Instantly share code, notes, and snippets.

@MangaD
Created August 9, 2026 10:38
Show Gist options
  • Select an option

  • Save MangaD/d89cce2d1b01def0c20165f31976fce8 to your computer and use it in GitHub Desktop.

Select an option

Save MangaD/d89cce2d1b01def0c20165f31976fce8 to your computer and use it in GitHub Desktop.
clangd and Language Servers: History, Architecture, LSP, and How Modern Code Intelligence Works

clangd and Language Servers: History, Architecture, LSP, and How Modern Code Intelligence Works

CC0

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.


1. Before language servers

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 values mean?"

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.


2. The old problem

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.


3. The language-server idea

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.


4. But we still have another problem

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.


5. Language Server Protocol

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)


6. "Server" doesn't mean Internet server

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.


7. What actually travels between them?

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)


8. Requests vs notifications

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)


9. The lifecycle

An LSP session looks roughly like this.

Startup

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)


10. Opening a file

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.


11. You type a character

Imagine:

foo.ba

becomes:

foo.bar

The 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.


12. Completion

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.


13. Diagnostics are slightly different

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

14. Now: what exactly is clangd?

clangd is the LLVM project's C-family language server.

clangd official site

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)


15. clangd's history

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)


16. clangd's superpower: the compiler AST

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.


17. C++ makes this brutally difficult

C++ tooling is unusually hard because meaning depends heavily on compilation context.

Consider:

#ifdef WINDOWS
using Handle = void*;
#else
using Handle = int;
#endif

What 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_X

Depends 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.


18. compile_commands.json

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)


19. CMake makes this easy

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.


20. What happens internally when clangd analyzes a file?

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.

Current-file understanding

clangd parses the file you're actively editing.

This gives extremely detailed information:

AST
types
expressions
overloads
templates
diagnostics
local variables

Project-wide understanding

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.


21. Indexing

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.


22. Background indexing

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.


23. Preamble optimization

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.


24. What clangd understands

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.


25. Hover

Suppose:

auto result = calculate();

You hover over:

result

clangd may know that:

result: std::optional<Result>

even though the source literally says:

auto

That's semantic analysis.


26. Go to definition

Suppose:

foo.calculate();

There could be 100 functions named:

calculate

in 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.


27. Rename

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.


28. Completion is compiler work

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.


29. LSP doesn't dictate implementation

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.


30. Think of LSP like an API

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.


31. Editor versus LSP client versus server

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.


32. Why Neovim became dramatically more IDE-like

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.


33. Where language servers are used

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.


34. Famous language servers

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.


35. Language servers aren't necessarily compilers

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.


36. Why not simply invoke the compiler constantly?

Imagine running:

clang++ main.cpp

after 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.


37. Incomplete code is normal

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.


38. Concurrency matters

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.


39. Latency is everything

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.


40. LSP's most important philosophical contribution

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.


41. Why clangd is particularly interesting

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.


42. Why clangd sometimes seems "wrong"

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.cpp

but clangd thinks it's:

clang++ main.cpp

Then 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.


43. The golden debugging question

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.


44. .clangd

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:
    - -Wall

But generally the best foundation is still:

accurate build system
        ↓
accurate compile_commands.json
        ↓
clangd

rather than manually recreating your entire build configuration.


45. LSP versus DAP

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.


46. Syntax highlighting isn't necessarily LSP

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)


47. LSP does not eliminate editor-specific behavior

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.


48. Extensions to LSP

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)


49. A useful mental model of clangd

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

50. A real example end-to-end

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.


51. Then you press "Go to definition"

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.


52. Then you rename Player

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.


53. Why this architecture spread so widely

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.


54. What LSP does not solve

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.


55. Why build systems are especially important for clangd

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.


56. clangd versus a C++ IDE

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.


57. Tree-sitter versus clangd

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.


58. Compiler, parser, indexer, language server

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.


59. What makes a good language server?

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.


60. The deeper historical significance of LSP

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.


61. One subtle downside

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.


62. Another downside: distributed-system-like complexity

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.


63. Debugging an LSP problem

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.

64. How I'd recommend learning clangd deeply

Instead of just configuring an editor, experiment with the architecture.

Start clangd with verbose logging:

clangd --log=verbose

Then 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:

clangd documentation

And for the protocol itself:

LSP specification


65. The whole thing in one diagram

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

66. The three ideas worth remembering

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

Why would a big IDE not use LSP internally?

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment