ANTIGRAVITY LABJP
Articles/Editor View
Editor View/2026-09-06Intermediate

The Morning My Completions Stopped: Code Was 1.4% of the Repo

When completions stop coming back in Antigravity, restarting the language server often does nothing. Here is how I split the slowness into three layers, counted each one, and asked git whether my ignore patterns were actually matching.

antigravity454language-server2lsp3workspace11typescript28antigravityignore2

I was reworking an article index page one morning when completions stopped coming back. Hover tooltips went blank too. Restarting the language server bought me a few minutes, and then it sank again in the same spot. On the third restart I stopped and sat back.

I had been looking for the cause inside configuration files. What I had never once looked at was the number sitting in front of all of that — how many files the editor was holding, and how few of them were actually code.

Here are the commands I ran that morning, and the numbers they gave me.

When a restart does not help, you are probably blaming the wrong layer

It is easy to say "the language server is slow" and leave it there. Inside the editor, though, at least three layers are looking at three different sets of files.

LayerWhat decides its scopeDoes a restart change the scope?
Language server projecttsconfig.json include / excludeNo — same config, same set of files, read again
File watching and workspace indexingWorkspace root and exclusion settingsNo
Agent code searchTracked files and .antigravityignoreNo

The symptoms usually point at a layer. Completions failing while search still works points at the first. The whole window freezing right after a save points at the second. Long waits only when you ask the agent to look something up points at the third. When all three feel slow at once, the shared volume is the more likely cause.

None of them shrink when you restart. Only the state gets rebuilt. So "it comes back for a few minutes" reads more honestly as: the state rebuilds fine, and then the same volume drags it back down to the same place.

Restarts are for symptoms; exclusions are for volume. Once I wrote that line down, counting became the first thing I do in the morning.

Start by counting the repository

The counting itself is two commands. Total tracked files, then how many of them are code.

# total tracked files
git ls-files | wc -l
 
# breakdown by extension, top entries only
git ls-files | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -8

The repository I work in is a Next.js site that keeps its articles as MDX. This is what came back.

GroupFilesBytesShare of bytes
All tracked files2,30034,383,649100%
.ts and .tsx71470,5101.4%
Under content/ (MDX)2,18331,874,97892.7%
Everything except content/ and public/content/1172,523,1527.3%

I always read file count and byte count together. Either one alone will send you after the wrong layer.

GroupDirectoriesFilesAverage file size
content/222,183about 14,600 bytes
src/4174about 6,600 bytes

content/ packs 2,183 files into 22 directories. To a watcher that registers interest per directory, that is cheap. To anything that walks per file, it is the whole cost of the repository. Lopsided shapes like this are where "watching is fine but indexing never finishes" comes from.

That table changed my mind on the spot. The language server project is 71 files. The include list in tsconfig.json is **/*.ts and **/*.tsx, so not a single MDX file ever reaches it.

The thing I had restarted three times was the lightest layer in the editor. The weight was in the other two, the ones that hold everything regardless of extension.

Keep a floor: how long it takes to simply read the files

To tell whether a change to scope did anything, the plainest possible measurement is enough. Open every file, read it, and stop there. No parsing, no indexing — a floor, not a benchmark.

import subprocess, time
 
out = subprocess.run(["git", "ls-files", "-z"], capture_output=True).stdout
files = [f.decode() for f in out.split(b"\0") if f]
code = [f for f in files if f.endswith((".ts", ".tsx", ".mjs", ".js"))]
 
def read_all(paths):
    start = time.perf_counter()
    for p in paths:
        with open(p, "rb") as h:
            h.read()
    return time.perf_counter() - start
 
for label, paths in (("all", files), ("code-only", code)):
    runs = sorted(read_all(paths) for _ in range(5))
    print(f"{label:10s} n={len(paths):5d} best={runs[0]*1000:.1f}ms median={runs[2]*1000:.1f}ms")

On my machine:

all        n= 2300 best=28.8ms median=29.4ms
code-only  n=   76 best=0.8ms median=0.8ms

Thirty-six times. I would rather be careful about how that number is read, though. The page cache was warm, and reading is all this does. Parsing, indexing, and embedding generation all stack on top of it, and a cold start or a burst of watch events costs many multiples of this floor.

So I never present this as proof of speed. I run the same command before and after changing exclusions, and I look only at how far the volume dropped. That way the answer arrives as a difference rather than as a feeling.

My exclusions were failing because of square brackets

Now for the exclusions. .antigravityignore uses gitignore syntax, which means git itself can tell you whether a pattern matches.

# write one pattern to a temp file and ask for a verdict
printf '%s\n' 'src/app/[locale]/' > /tmp/probe-ignore
git -c core.excludesFile=/tmp/probe-ignore \
    check-ignore --no-index -v 'src/app/[locale]/HomeClient.tsx'

Nothing came back. No match. I lined up a few paths that should not have matched either, and got this.

Patternsrc/app/[locale]/HomeClient.tsxsrc/app/l/HomeClient.tsxsrc/app/x/page.tsx
src/app/[locale]/no matchmatchno match
src/app/\[locale\]/matchno matchno match
**/[locale]/**no matchmatchno match

The brackets were being read as a character class. [locale] means "any one of l, o, c, a, e", so it matches src/app/l/ and src/app/c/ while leaving the real src/app/[locale]/ untouched. The directory I meant to exclude stayed in, and directories I never intended to touch went out.

With the App Router those brackets are everywhere. In my repository, 21 tracked paths contain them, and [category] and [slug] behave exactly the same way.

The fix is to escape the brackets, or to name a parent directory that has none. Either way, count what is left afterwards.

# how many files survive the exclusions
TOTAL=$(git ls-files | wc -l)
IGN=$(git ls-files -z \
      | git -c core.excludesFile=/tmp/probe-ignore check-ignore --no-index --stdin -z \
      | tr -cd '\0' | wc -c)
echo "tracked=$TOTAL ignored=$IGN remaining=$((TOTAL-IGN))"

With just two lines, content/ and public/content/, mine printed tracked=2300 ignored=2183 remaining=117. One command tells you whether the drop matches what you expected.

If the matching itself is what keeps failing, Four Reasons Your .antigravityignore Rules Are Not Taking Effect covers slash placement and negation ordering, which narrows things down faster.

A word on how I take the measurement. I run it five times and report both the best and the median. A single run can land on the moment some other process happens to be busy, and if those two values are far apart I go looking at what else is running before I touch scope at all.

And if the full set and the code-only set come out close to each other, volume is not the problem. That is when I switch to hunting for one pathological file — a generated bundle, or a file with a single enormous line. The same 30 milliseconds means something entirely different depending on whether it is spread across two thousand files or concentrated in one.

What I exclude, and what I deliberately leave in

Once you know the volume, the next question is how much of it to remove. I sort candidates into three tiers.

TierExamplesHow I treat it
Generated output.next/, out/, build artifacts, coverageExclude without hesitation — it can be regenerated
Dependenciesnode_modules/Usually excluded by default already; no need to repeat it
Things you wroteMDX under content/, design notesCheck what stops being searchable before excluding

The third tier is where I got it wrong. I excluded all of content/ in one go, and then asked the agent whether an article on a given topic already existed. It found nothing. The volume dropped nicely, and the task I lean on most often quietly stopped working.

A cheap way to avoid that trade is to record one search you actually rely on, before and after.

# note how many hits the search you depend on returns, before excluding anything
git grep -l 'antigravityignore' -- content/articles/ja | wc -l

Mine returned 26. If the same command comes back smaller after the change, the exclusion went too far.

The other habit worth keeping is separating exclusions by purpose. Making file watching lighter and narrowing the agent's search surface are two different goals, and they live in different places. Copying the same few lines into both tends to overshoot on one side and fall short on the other.

Where I draw the line now

My morning routine got shorter after this. When completions stop, I put the tracked file count and the code file count side by side. If there are two orders of magnitude between them, the problem is not the language server — it is whichever layer around it is holding everything.

The reverse case is real too. In a repository with several thousand actual source files, exclusions are not the lever; narrowing the tsconfig.json include list is. Counting once is what tells you which situation you are in.

If you do one thing today, print git ls-files | wc -l next to the number of .ts and .tsx files, in the same view. If you would rather not leave the editor for it, the sidebar terminal I wrote about in Building a Verification Loop With Antigravity 2.10.0's Embedded Terminal handles this well.

I write those two numbers down before I add a single line of exclusions. Thank you for reading this far.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Editor View2026-04-21
Antigravity IDE: Diagnosing Unresponsive Language Servers and Missing IntelliSense
A structured diagnostic flow for when the TypeScript, Pylance, gopls, or rust-analyzer language server goes silent inside Antigravity IDE — covering cache corruption, extension conflicts, and memory limits.
Editor View2026-05-07
Fixing 'Find References' in Antigravity When Results Are Empty or Incomplete
When Find References returns nothing or only some of the call sites in Antigravity, the cause depends on whether the language server or the workspace index is silent. This guide walks through the diagnosis for TypeScript, Python, and monorepo setups.
Editor View2026-07-08
When Antigravity Reads Cloud-Synced Files as Empty: The Online-Only Placeholder Trap
A file is right there in Finder, yet the Antigravity agent insists it is empty or missing. The culprit is an online-only placeholder created by cloud sync. Here is how to spot it, hydrate the data, and design a workspace that avoids the problem.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →