A plain blame answers "who changed line 40 of file X". Lens answers the question you actually have: what happened to this particular function, even when the file was renamed along the way and the code moved to another module. That includes Unreal Blueprints, not just code. Below, in detail: what it tracks, in which languages, how it matches and where its limits are. No promises beyond what ships.
You ask about a function by name. You get its whole life story, stitched across every rename and every move between files. Purely cosmetic changes (formatting, comments, variable names) don't clutter the result.
tq lens blame --symbol calculate_damage CREATED ch-6f9 rev:1 chris "first version" MODIFIED ch-a12 rev:3 marta "damage buff" MOVED+RENAMED ch-c73 rev:5 chris (from compute_damage in Weapon.rs) [~86%]
The number in brackets, for example [~86%], is the match confidence. When the name and code match exactly, it's 100%. When a function was renamed and rewritten at once, Lens shows how sure it is that it's still the same thing.
Lens recognizes and tracks code symbols, that is, the named elements of a program. Eight kinds specifically:
Free functions and methods inside classes. A method carries a qualified name, for example Enemy::TakeDamage, so two different methods with the same name don't get confused.
Classes, structs, enums, interfaces and traits (in Rust). Tracked the same way as functions, across renames and moves.
Units that group code (module, namespace, package), as far as the language expresses them.
Unreal Blueprints (.uasset): EventGraph, functions, macros and the ConstructionScript. "Who changed Interact in BP_Door?" works without a single line of code.
For most languages Lens uses a full syntax parser (tree-sitter), so it understands the structure of the code rather than guessing from text. A few languages without a ready parser are handled by a simpler fallback.
| Method | Languages | Accuracy |
|---|---|---|
| Full parser (tree-sitter) | Rust, C, C++, C#, Python, JavaScript, TypeScript, Go, Java, Lua | high |
| UE Blueprints (native) | .uasset / .umap files — Blueprint graphs: EventGraph, functions, macros, ConstructionScript | graphs |
| Fallback extractor (heuristic) | Verse (UEFN), GDScript, ActionScript, HLSL / GLSL / USF / USH shaders | approximate |
The fallback extractor reads code line by line and recognizes declarations from indentation and keywords. It works, but it's sensitive to unusual formatting, one-line blocks or mixing tabs with spaces. File extensions can also be mapped by hand in a .tqlens.toml file (for example, treat .inc as C++).
The heart of Lens: it recognizes that a symbol is still the same symbol, even after sizeable changes. It detects:
| Situation | What Lens does |
|---|---|
| Renaming a file, or moving a function to a different file | Keeps tracking it, regardless of the path. |
| Renaming the function itself | Recognizes it by code structure, with a confidence score. |
| Renaming and rewriting the body at once | Matches fuzzily, shows the confidence (for example [~72%]). |
| Swapping the names of two functions | Resolves it by structure, not by name. |
| Extracting part into a new function, or inlining it back | Detects it as a split or a merge. |
| Copying a function | Marks it as a copy of the still-living original. |
Lens reduces code to a normalized form: variable names become anonymous placeholders, formatting and comments are dropped. So renaming variables inside a function doesn't lose its identity. It even holds up against a cascade of renamed called functions.
When two functions match almost equally well, Lens doesn't quietly pick one. It flags the result as ambiguous and shows the candidates. You resolve it yourself with tq lens link or one click in Studio, and your decision beats the heuristic.
When a function disappears in one commit and reappears in another within 7 days (the window is configurable), Lens glues it into a single move instead of showing a separate "deleted" and "created".
A damage method in an Unreal project: it starts in one file, gets moved to a separate module along the way, and is renamed. A plain blame would lose the trail at each of those steps. Lens glues it into a single life story:
tq lens trace --symbol ApplyDamage CREATED rev:1 Source/Combat/Enemy.cpp TakeDamage MODIFIED rev:4 Source/Combat/Enemy.cpp (damage balancing) MOVED rev:7 Source/Combat/DamageSystem.cpp (from Enemy.cpp) [~100%] RENAMED rev:9 Source/Combat/DamageSystem.cpp TakeDamage → ApplyDamage [~91%]
The move between modules is recognized by an exact fingerprint (100%), while a rename together with a small body edit gets a confidence score (91%). When you later extract part of that method into its own function, Lens shows it as a split:
tq lens blame --symbol UpdateCooldowns
CREATED rev:12 Source/Combat/DamageSystem.cpp (extracted from ApplyDamage) [~78%]UCLASS, UFUNCTION, UPROPERTY, GENERATED_BODY). A symbol's history lives where the logic does, in your methods, no matter which file or module you move them to.| Command | What it does |
|---|---|
tq lens blame --symbol <name> | History of a symbol. Takes the full name or just the tail (TakeDamage finds Enemy::TakeDamage). |
tq lens trace --symbol <name> | The full life story across every name the symbol carried along the way. |
tq lens symbols <file> | List of symbols in a file at the current revision. |
tq lens link <newer> <older> | Manually stitch two histories when the heuristic hesitated. unlink undoes it. |
tq lens reindex / index | Rebuild the index, or pull in pending revisions. |
Every command accepts --json, so Lens plugs easily into scripts and tools.
Lens: name label (CodeLens). A click opens the symbol's history. The same works from the context menu on the symbol under the cursor, with the result in a panel. Underneath it's tq lens blame --json, so the editor and the terminal show exactly the same thing. It's the only ready plugin today; full Visual Studio and JetBrains Rider are planned (see "Roadmap" below).Parse results are cached under the file's content fingerprint. Because storage addresses files by their content, a file that's unchanged, or identical to another, isn't parsed a second time.
| Indexing 64 files | Time |
|---|---|
| first time (cold cache) | 135 ms |
| again (warm cache) | 19 ms |
Lens understands text. Binary files, like Blueprint or .uasset, are a different story, and here it's worth being honest.
Lens has a working plugin host (WASM) together with a toolkit (SDK) for writing your own symbol extractor for any format, binary included. Such an extractor computes its own fingerprint, and Lens tracks those symbols just like text ones: across renames and moves.
Lens doesn't read Blueprints or .uasset files yet. Blueprint support is on our development roadmap, and it will be built on exactly this plugin mechanism. For now Lens tracks symbols in text code.
We keep an honest line between what works today and what's still planned. Near the top of the list:
Tracking symbols right inside Blueprint graphs and .uasset files, built on the existing plugin mechanism. The history of a Blueprint node or function, just like for text code.
Alongside the existing VS Code extension, we're planning plugins for full Visual Studio and JetBrains Rider, so a symbol's history is at hand where you actually write game code.
So there are no surprises, here are the things Lens doesn't do, or does with caveats:
Want to see Lens on your own repo? Get in touch. More on the day-to-day is in the documentation.