cdot is a (proof of concept) language server proxy for C that adds UFCS-style (Uniform Function Call Syntax) dot completions on top of clangd.
It sits between your editor and clangd, passing everything through unchanged — except dot completions, where it injects free functions that follow the type_function naming convention as if they were methods.
struct car { int speed; };
void car_set_speed(struct car *c, int speed);
int main() {
struct car my_car;
my_car.set_speed( // ← cdot suggests this; clangd alone wouldn't
}When you accept the set_speed completion, cdot rewrites it to the correct C call:
car_set_speed(&my_car, ) // cursor lands inside the parenscdot recognises a function as a UFCS candidate for type T when:
- Its name starts with
T_(e.g.car_set_speedfor typecar) - Its first parameter is a pointer to
T— eitherstruct T*or a typedef (T*)
Zed ──LSP──► cdot ──LSP──► clangd
│
└─ intercepts textDocument/completion
injects UFCS items into the response
Three async tasks run concurrently:
| Task | Role |
|---|---|
| client handler | Reads LSP messages from the editor, maintains a document store, resolves dot completions |
| clangd writer | Drains an mpsc channel into clangd's stdin |
| clangd router | Reads clangd's stdout; augments tracked completion responses before forwarding |
- Editor sends
textDocument/completion(dot-triggered or ctrl+space) - cdot scans the stored document text to find the variable name before the dot
- Resolves the variable's declared type by scanning backwards for its declaration
- Scans all open documents for functions matching
<type>_*with a pointer first parameter - Stores the candidates keyed by the request ID
- Forwards the request to clangd unchanged
- When clangd's response arrives, appends the UFCS items before sending to the editor
Type resolution and candidate scanning are purely text-based — no clangd indexing required, so they work instantly on freshly opened files.
git clone https://github.com/dxkyy/cdot
cd cdot
cargo build --releaseAdd target/release/cdot (or target\release\cdot.exe on Windows) to your PATH.
The extension/ directory contains a Zed extension that registers cdot as the language server for C files.
- Open Zed → Extensions → Install Dev Extension
- Select the
extension/directory inside this repo
Add this to your Zed settings.json:
"languages": {
"C": {
"language_servers": ["cdot", "..."]
}
}The "..." keeps any other configured servers active. (However this isnt recommended, as clangd is Zed's default LSP for C, meaning cdot and clangd would run in parallel, causing weird bugs and duplicate code completions.)
Configure cdot through Zed's LSP settings. All options live under initialization_options:
"lsp": {
"cdot": {
"initialization_options": {
"clangdPath": "clangd-18",
"clangdArgs": ["--query-driver=/usr/bin/*", "--clang-tidy"],
"fallbackFlags": ["-std=c23"]
}
}
}| Option | Type | Default | Description |
|---|---|---|---|
clangdPath |
string | "clangd" |
Path or name of the clangd binary to spawn |
clangdArgs |
string[] | [] |
Extra CLI arguments passed to clangd at startup |
| anything else | — | — | Forwarded to clangd as initializationOptions (e.g. fallbackFlags) |
cdot reads clangdPath and clangdArgs before spawning clangd, so they take effect at startup. All other keys pass through transparently and are handled by clangd directly.
- Naming convention required. Functions must follow
<type>_<verb>with a pointer-to-type first parameter. Arbitrary free functions are not suggested. - Text-based only. Type resolution is a backward scan for declarations; it does not use semantic analysis. Complex cases (casts, macros, chained calls) may not resolve.
- Single-file scan. Candidates are found by scanning documents open in the editor. Functions defined only in headers that haven't been opened won't appear.
- Pointer vs. value detection is based on whether
*appears in the declaration text; it does not handletypedefd pointer types (e.g.typedef struct car* Car).