LmCast :: Stay tuned in

Magic Move for Code Snippets from first principles

Recorded: Sept. 9, 2026, 6 p.m.

Original Summarized

Magic Move for Code Snippets from first principles

Rahul Ravikumar's Blog

More Articles
Talks

September 06 2026, Monday
Magic Move for Code Snippets from first principles

Introduction
I have been using a Framework 16 as my daily driver ever since its launch. For a long time, I maintained a fragile truce dual-booting Ubuntu and Windows. But in late 2025, I finally pulled the plug, wiped both, and went all-in on CachyOS. Why I switched is probably worth a dedicated blog post, but the short version: I couldn't be happier. Arch / CachyOS has been an absolute breath of fresh air.
At work, my daily driver is an M5 MacBook. Lately, macOS has felt increasingly frustrating to live with. How Apple managed to trade away desktop performance in favor of Liquid Glass eye candy is a compromise I will never quite understand.
Moving to Linux full-time was surprisingly painless. Most tools I rely on had native Linux builds, and for the few that didn't, solid alternatives stepped in. But there was one painful sacrifice that gave me pause: Apple Keynote (and, more specifically, Magic Move).
As someone who speaks at conferences fairly often, presentation tooling isn't vanity - it's narrative scaffolding. For years, I had the luxury of leaning on Keynote to make complex technical concepts look effortless. Walking away from that workflow stung.
Magic Move, and Its Shortcomings
Yet for all its brilliance, Magic Move has always had a glaring blind spot: animating code snippets.
If you've ever tried to transition between two snippets of code in Keynote, you know the horror. Keynote doesn't understand code; it understands glyphs. Watch in dismay as the val from line 1 flies diagonally across the slide to morph into the al of calculate(), while punctuation scrambles like alphabet soup.
To make it look professional, you're forced into brittle, unhinged workarounds: splitting lines into invisible bounding boxes, inserting transparent dummy characters, and pixel-nudging tokens by hand just to show a clean three-line refactor. You can make it look magical, but only through sheer brute force. Still, the payoff was real: after almost every conference talk, people would come up not to ask about the architecture, but to ask: "How on earth did you do those cool animations?"
Working on the Jetpack Compose team, a UI toolkit that is now fully multiplatform - this comes up constantly. In fact, it's practically an annual tradition: every Google I/O or major conference season, someone on the team inevitably stares at a mangled transition and declares: "What if we just wrote our entire slide deck in Compose so code animates properly?"
A few brave souls have actually done it using Compose's Shared Element transitions. I wasn't quite brave enough to abandon WYSIWYG tooling entirely - Keynote is still unmatched for layout, typography, and the parts of your presentation that are not code (even if Apple is slowly gatekeeping more templates behind a subscription).
The Trigger
A couple of weeks ago on HN, there was a project called Bento that really caught my attention. This is a fully local, offline and an extensible presentation toolkit. The project is very slick, and definitely worth paying attention to. Bento supports morph already for shapes and mathematical expressions. You can tween position, colors & even gradients. The only feature missing from my perspective was to be able to support morphing code snippets.
Research
I decided to roll up my sleeves and see what it would take to build this. The first step in a project like this is to survey the existing literature. It turns out there is plenty, given that diffing content is a well-trodden problem. We do this every day as developers whenever we run git diff, for example.
The usual suspects are Myers and Patience diff, both of which compute the shortest edit distance between two sequences. There have also been previous attempts at this problem by members of the Kotlin / Android community (such as Re-creating Magic Move with Compose).
Intuition: When Math Makes for Bad Animations
Having looked at past attempts, my intuition was that Myers diff was solving the wrong problem for this use case.
Myers diff is mathematically elegant: it finds the shortest sequence of edits to turn String A into String B. But the mathematically optimal diff often makes for a visually jarring animation. If a variable is renamed or moved, Myers might choose to delete three characters here, insert two there, and slice an identifier in half because it saves a single edit step.
In an animation, human perception matters far more than minimal edit distance. You don't want minimal operations; you want visual continuity. You want tokens to glide smoothly to their new positions or fade away gracefully.
That led me to a 1978 paper by Paul Heckel: A technique for isolating differences between files (Communications of the ACM, Volume 21, Issue 4, 1978). This paper approached diffing from a completely different perspective. The algorithm runs in O(n) time and feels tailor-made for animations. Given my familiarity with Kotlin and Jetpack Compose, I decided to experiment with adapting this paper to compute differences between two token streams, rather than treating code snippets as plain text.
Defining what to diff
Our first step is to use a tokenizer to convert the code snippet into a token stream.
When thinking about tokenizing code, the immediate instinct is often: "Let's build/use an AST."
But, an AST is a trap for code snippets because:

We want to support multiple programming languages without bundling a heavy parsers / compiler infrastructure.
Code snippets are inherently messy. Speakers frequently use partial snippets, omit imports or trailing braces, and lean on pseudo-code that would cause a strict parser to blow up.

Instead, what we want is a general representation of what tokens and fragments mean. Text editors face the exact same challenge: they need fast, fault-tolerant syntax highlighting across dozens of languages. One of the many ways that they solve this is using TextMate grammars. Originally designed for TextMate (the legendary macOS text editor), this format was later adopted by Sublime Text, Visual Studio Code, and Eclipse, making it a de facto industry standard.
This grammar is effectively a prioritized list of regular expressions that can identify each token and assign it a scope. So, if you have a snippet that looks something like:
val x = 10
print(x)
The token stream looks something like:
Token(content='val', scopes='[source.kotlin, storage.type.kotlin]', lineNumber=0, startIndex=0, endIndex=3)
Token(content=' x ', scopes='[source.kotlin]', lineNumber=0, startIndex=3, endIndex=6)
Token(content='=', scopes='[source.kotlin, keyword.operator.assignment.kotlin]', lineNumber=0, startIndex=6, endIndex=7)
Token(content=' ', scopes='[source.kotlin]', lineNumber=0, startIndex=7, endIndex=8)
Token(content='10', scopes='[source.kotlin, constant.numeric.integer.kotlin]', lineNumber=0, startIndex=8, endIndex=10)
Token(content='print', scopes='[source.kotlin, support.function.kotlin]', lineNumber=1, startIndex=0, endIndex=5)
Token(content='(', scopes='[source.kotlin, meta.group.kotlin, punctuation.section.group.begin.kotlin]', lineNumber=1, startIndex=5, endIndex=6)
Token(content='x', scopes='[source.kotlin, meta.group.kotlin]', lineNumber=1, startIndex=6, endIndex=7)
Token(content=')', scopes='[source.kotlin, meta.group.kotlin, punctuation.section.group.end.kotlin]', lineNumber=1, startIndex=7, endIndex=8)
The Token class can be represented by something like:
class Token(
/** The actual content of the parsed token. */
val content: String,
val scopes: List<String>,
val lineNumber: Int,
val startIndex: Int,
val endIndex: Int
) {
// ...
}
TextMate scopes give us the full picture of what a Token represents. In our example, the first scope is always the root ( source.kotlin), signaling that we are looking at Kotlin source code. The last scope in the list tells us the specific role the token plays (e.g. storage.type.kotlin or constant.numeric.integer.kotlin). TextMate defines a comprehensive hierarchy of standard scopes, which is how text editors decide what color to paint each token.
Structural similarity
As we diff the sequence of Tokens to produce a great Magic Move effect, it becomes clear that line numbers and character offsets shouldn't dictate structural identity. Tokens are free to move across lines-and during a presentation showing a refactor, they frequently do.
The traits that truly define a Token when diffing are its content, its primary scope (the last entry in the list), and its depth (i.e. scopes.size). The depth provides structural insights-distinguishing, for instance, a token at top-level scope from one that is nested deeply inside a function body.
So our Token class actually becomes:
class Token(
/** The actual content of the parsed token. */
val content: String,
/** The primary scope */
val scope: String,
/** The depth of the primary scope. */
val depth: Int,
/* More context for animations (but NOT structural similarity). */
val lineNumber: Int,
val startIndex: Int,
val endIndex: Int
) {
// ...
}
The Heckel Diff Algorithm
1. Finding Anchors (Islands of Certainty)
At its core, the Heckel diff algorithm is simple. The first goal is to find anchors - tokens that appear exactly once in both the previous and current sequences. If a token only occurs once in both snippets, we can be confident that it represents the exact same token before and after the transformation.
Consider two snippets: previous and the transformed current.
// Previous
val x = 10
produces,
Token(content='val', scope='storage.type.kotlin', depth=2, lineNumber=0, startIndex=0, endIndex=3)
Token(content=' x ', scope='source.kotlin', depth=1, lineNumber=0, startIndex=3, endIndex=6)
Token(content='=', scope='keyword.operator.assignment.kotlin', depth=2, lineNumber=0, startIndex=6, endIndex=7)
Token(content=' ', scope='source.kotlin', depth=1, lineNumber=0, startIndex=7, endIndex=8)
Token(content='10', scope='constant.numeric.integer.kotlin', depth=2, lineNumber=0, startIndex=8, endIndex=10)
and:
// Current
val x = 20
produces,
Token(content='val', scope='storage.type.kotlin', depth=2, lineNumber=0, startIndex=0, endIndex=3)
Token(content=' x ', scope='source.kotlin', depth=1, lineNumber=0, startIndex=3, endIndex=6)
Token(content='=', scope='keyword.operator.assignment.kotlin', depth=2, lineNumber=0, startIndex=6, endIndex=7)
Token(content=' ', scope='source.kotlin', depth=1, lineNumber=0, startIndex=7, endIndex=8)
Token(content='20', scope='constant.numeric.integer.kotlin', depth=2, lineNumber=0, startIndex=8, endIndex=10)
Every token in both lists except for the last constant ( 10 vs 20) occurs only once, so they become our anchors and share the same identity.
2. Forward Pass (Marching forward)
Once we have our anchors pinned down, we look at their immediate neighbors.
For each anchor, we check if the token immediately following it in current ( currentIdx + 1) matches the token immediately following it in previous ( previousIdx + 1) sequence. If they match, they are paired up. We continue walking forward until the tokens diverge.
3. Backward Pass (Marching backward)
Next, we do the forward pass in reverse. Starting from our anchors, we check offsets currentIdx - 1 and previousIdx - 1. If the preceding tokens match, we pair them up and keep walking backward.
4. Final Pass
By the end of these passes, all surviving and repositioned tokens have been matched. Whatever remains unmatched in previous is marked as a deleted token, and whatever remains unmatched in current is marked as an inserted token.
A slightly more complicated example
If you have:
// Previous
val x = 10
fun convert(input: Int) {
// ...
}
and :
val x = 10
fun convert(input: Int) {
println("The actual implementation")
}
The actual changes look like:
Match(previous=Token(content='val', scope='storage.type.kotlin', depth=2, lineNumber=0, startIndex=0, endIndex=3), previousIdx=0, current=Token(content='val', scope='storage.type.kotlin', depth=2, lineNumber=0, startIndex=0, endIndex=3), currentIdx=0)
// ...
Match(previous=Token(content=' ', scope='meta.block.kotlin', depth=3, lineNumber=2, startIndex=0, endIndex=2), previousIdx=16, current=Token(content=' ', scope='meta.block.kotlin', depth=3, lineNumber=2, startIndex=0, endIndex=2), currentIdx=16)
Delete(token=Token(content='//', scope='comment.line.double-slash.kotlin', depth=5, lineNumber=2, startIndex=2, endIndex=4), index=17)
Delete(token=Token(content=' ...', scope='punctuation.definition.comment.kotlin', depth=4, lineNumber=2, startIndex=4, endIndex=8), index=18)
Insert(token=Token(content='println', scope='support.function.kotlin', depth=4, lineNumber=2, startIndex=2, endIndex=9), index=17)
Insert(token=Token(content='(', scope='punctuation.section.group.begin.kotlin', depth=5, lineNumber=2, startIndex=9, endIndex=10), index=18)
Insert(token=Token(content='"', scope='punctuation.definition.string.begin.kotlin', depth=6, lineNumber=2, startIndex=10, endIndex=11), index=19)
Insert(token=Token(content='The actual implementation', scope='string.quoted.double.kotlin', depth=5, lineNumber=2, startIndex=11, endIndex=36), index=20)
Insert(token=Token(content='"', scope='punctuation.definition.string.end.kotlin', depth=6, lineNumber=2, startIndex=36, endIndex=37), index=21)
Insert(token=Token(content=')', scope='punctuation.section.group.end.kotlin', depth=5, lineNumber=2, startIndex=37, endIndex=38), index=22)
Match(previous=Token(content='}', scope='punctuation.section.group.end.kotlin', depth=4, lineNumber=3, startIndex=0, endIndex=1), previousIdx=19, current=Token(content='}', scope='punctuation.section.group.end.kotlin', depth=4, lineNumber=3, startIndex=0, endIndex=1), currentIdx=23)
You can see that tokens match cleanly across both sequences right up to the block body.
We Delete the comment tokens ( // and ...) and Insert the new println(...) call. Crucially, the trailing } matches cleanly because it served as an anchor point at the end of the block.

Pro-tip for smooth animations: If you animate deletions token-by-token independently, the animations look unorchestrated. In our implementation contiguous deleted tokens are batched together so they fade out in unison, while newly inserted tokens fade in smoothly. Anchored tokens glide gracefully to their new coordinates. The result feels cohesive and cinematic rather than chaotic.

Source Code
The full implementation is available at https://github.com/tikurahul/warp.
Epilogue
I also managed to contribute to Bento. Bento release 1.0.19 includes the ability to morph code snippets.
Here is the link to my pull request.

Rahul Ravikumar  
GitHub   |  
Bluesky   |  
LinkedIn   |  
Mastodon   |  
Svbtle   |  
Twitter

The motivation for developing a method for animating code snippets stems from the limitations encountered in presentation tooling, specifically Magic Move in Keynote, which fails to animate code meaningfully because it processes glyphs rather than code structure. The author observed that existing methods for animating code rely on brittle workarounds, forcing users to manually manipulate spacing and characters, rather than reflecting true structural changes in the code. This led to the central challenge: creating smooth, perceptually continuous transitions for code refactoring in presentations.

The core of the solution requires moving beyond traditional text diffing algorithms, such as Myers or Patience diff, which focus purely on minimizing the edit distance between two sequences. The author intuitively recognized that minimizing edit distance often results in visually jarring animations because human perception demands visual continuity over minimal textual operations. Therefore, the approach shifts focus from minimal edit distance to animating the perceived structure of the code.

To achieve this, the code snippets must first be converted into a stream of tokens rather than being treated as plain text. An Abstract Syntax Tree approach is deemed unsuitable because it requires heavy parsing infrastructure that complicates support for multiple languages and struggles with the inherently messy nature of code snippets, which often involve partial code or pseudo-code. Instead, the author proposes using a token stream inspired by TextMate grammars, a format widely adopted by text editors, to represent the structure of the code. Each token is defined by its content, a hierarchy of scopes reflecting its context (like language and syntactic role), line number, and character indices.

For animation purposes, the traits that define the structural identity of a token are redefined. Line numbers and character offsets are considered flexible during an animation, as tokens are free to move across lines. Structural identity is determined by the token's content, its primary scope, and its depth, which indicates its nesting level within the structure.

The differentiation between tokens in the previous and current sequences is then handled by a specialized algorithm, termed the Heckel diff algorithm, designed for animation realism. This algorithm proceeds in several stages. First, it identifies anchors, which are tokens that appear exactly once in both input sequences, providing certainty about their identity. Next, a forward pass marches through the sequences, looking for contiguous matches between anchors and their immediate neighbors to pair up similar tokens. Subsequently, a backward pass performs the same matching in reverse, ensuring comprehensive pairing. Finally, the algorithm determines deletions (unmatched tokens in the previous sequence) and insertions (unmatched tokens in the current sequence).

This process allows the algorithm to mark individual tokens as deleted or inserted, providing a precise representation of the transformation. For smooth visual effects, the author suggests a technique where contiguous deleted tokens are batched to fade out simultaneously, and newly inserted tokens fade in progressively, while anchored tokens glide smoothly to their new positions. This batched approach prevents the chaotic appearance that results from animating each change independently, yielding a cohesive and cinematic result. The implementation of this technique is made available through the warp project, and the capability to morph code snippets has been integrated into the Bento presentation toolkit.