The overlooked value of structural search and replace
Field Notes on JetBrains AI Assistant

The overlooked value of structural search and replace

It's been sitting in the IDE for two decades doing exactly what people now reach for an AI prompt to do worse.

Somewhere around the fourth time I asked AI Assistant to rewrite the same defensive-null-check pattern across a dozen files, and got a subtly different rewrite in three of them, I remembered that JetBrains has shipped a tool that does exactly this task correctly, every time, since long before any of this existed. Structural search and replace, buried under Edit → Find, has been sitting there the whole time.

What it actually does differently

Regular find-and-replace matches text. Structural search and replace matches the syntax tree, which is a different and much stronger guarantee: it understands that if (x != null) { return x.getName(); } and if (null != x) { return x.getName(); } are the same pattern regardless of operand order, that a variable can be named anything and the pattern still matches, that whitespace and formatting don’t matter because it isn’t looking at characters at all.

An AI rewrite, by contrast, is fundamentally probabilistic. Ask it to apply the same transformation across forty call sites and, over forty, you will eventually get one that’s subtly different, not because the model is bad but because generation doesn’t guarantee the kind of exactness a syntax-tree match guarantees by construction. For code you’re about to run in production, that difference matters more than it sounds like it should.

The kind of task this actually wins

Renaming a pattern rather than a symbol. IntelliJ’s built-in rename handles a symbol perfectly; it has no answer for “every place we construct this object and then call .setActive(true) immediately after, do it in the constructor instead.” Structural search and replace handles exactly that, matching the whole shape, not just the name.

Migrating an idiom across a codebase. Old-style null checks to Optional. Manual builder calls to a fluent chain. Anywhere the team agreed on a new pattern and now has months of old code following the previous one, this is the tool that finds every instance without asking a model to infer intent forty separate times and hoping the inferences stay consistent.

And auditing for a pattern you’re worried about, not just replacing it. Run a structural search alone, no replace, to find every place a certain kind of resource gets opened without a corresponding close, or every catch block that swallows an exception silently. That’s a different use case from refactoring and it’s arguably the more valuable one: finding out how widespread a problem actually is before deciding what to do about it.

Where the AI assistant is still the better call

Anything where the transformation genuinely depends on judgment rather than shape. Structural search and replace can find every function over a certain length; it has no opinion about how to split one well, because splitting well requires understanding what the function is actually for, not just how it’s shaped.

Anything where the pattern is loose enough that describing it precisely as a syntax template is harder than just explaining the intent in a sentence. If you find yourself fighting the template syntax for ten minutes to express something you could say to the AI assistant in one sentence, that’s the signal you picked the wrong tool for this particular case.

And one-off changes. Structural search and replace pays for itself at scale, a pattern repeated tens of times across a codebase. For a single instance, writing the template costs more than just making the edit by hand.

The genuinely underused half: template variables

The feature inside structural search and replace that almost nobody touches is template variables, and it’s the difference between a rigid pattern and a genuinely powerful one.

A basic search matches foo.bar() literally. A search with a variable matches $INSTANCE$.bar() for any expression in that position, and the same variable, reused in the replace template, carries that captured expression through to the output. That’s what turns “find this exact code” into “find this shape, wherever it appears, with whatever’s actually there.” Constraining the variable further, to a specific type, to a specific count of matches, is what separates a template that fires false positives across the codebase from one that fires exactly where it should and nowhere else.

Most people who’ve tried the feature once, hit a limitation with the basic literal-match version, and concluded it wasn’t powerful enough never got to this part. It’s the part that makes the tool actually competitive with what an AI rewrite is being asked to do, minus the variability.

The honest comparison

Structural search and replace is not a substitute for AI assistance in general, and this isn’t an argument that it should be. It’s a substitute for one specific, narrow, high-frequency task: applying an exact, well-defined transformation across many locations, where correctness matters more than the transformation being clever.

For that task specifically, a deterministic tool that’s been sitting in the IDE for two decades is simply the better choice than a probabilistic one, even a very good probabilistic one. Worth remembering before reaching for the AI panel out of habit for something the Find menu has always been able to do exactly right.

More field notes on JetBrains AI Assistant

This piece is one entry in a running series on how AI coding tools change day-to-day engineering work. For more practical notes on JetBrains AI Assistant specifically, browse the full set at /blog/tag/jetbrains-ai/. For the wider view across every tool in the stack, the AI coding tag collects the whole archive in one place.

Get the next live webinar in your inbox

One email a month: the upcoming live event + free recording access for subscribers. No spam, unsubscribe anytime.