RTags: Echte C++-Codenavigation
In meinem Ripgrep-Artikel
habe ich geschrieben, dass rg nicht zuverlÀssig zwischen der Definition
einer Funktion, ihrer Deklaration und einem Aufruf davon unterscheiden
kann â Regex ist eine Textsuche, kein C++-Parser. RTags ist das Werkzeug,
zu dem ich greife, sobald genau diese Unterscheidung wichtig wird: Es
indiziert C/C++-Quellcode mit libclang, demselben Parser, den Clang
selbst zum Kompilieren verwendet. Die Antworten kommen also aus einem
tatsÀchlichen VerstÀndnis der Sprache, nicht aus einem Mustervergleich.
Was RTags eigentlich ist
RTags ist ein Client/Server-Paar. rdm ist ein Daemon, der das Projekt mit
libclang parst und das Ergebnis â einen Index aus jedem Symbol, jeder
Definition, Deklaration und Referenz â im Speicher und auf der Festplatte
hĂ€lt. rc ist ein kleiner Kommandozeilen-Client, der ĂŒber einen lokalen
Socket mit rdm spricht und Fragen stellt: Wo ist das definiert, wer ruft
das auf, wofĂŒr steht dieses Symbol.
Weil rdm das echte Clang-Frontend verwendet, muss es genau wissen, wie
jede Datei kompiliert wird â dieselben Include-Pfade, Defines und
Standardversion, die der tatsÀchliche Build verwendet. Genau das liefert
eine Compilation Database (compile_commands.json), und das ist der
eine Einrichtungsschritt, den RTags wirklich braucht.
Eine Compilation Database erzeugen
Verwendet das Projekt CMake, ist das ein einziger Schalter:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build
CMake schreibt build/compile_commands.json, eine Liste jeder
Ăbersetzungseinheit mit dem exakten Compiler-Aufruf, mit dem sie gebaut
wurde.
FĂŒr ein reines Makefile-Projekt fĂ€ngt
bear die tatsÀchlichen
Compiler-Aufrufe wÀhrend eines echten Builds ab und erzeugt dieselbe
Datei:
bear -- make
In beiden FĂ€llen entsteht eine JSON-Datei, auf die RTags verweisen kann â welches Build-System sie erzeugt hat, spielt keine Rolle.
Daemon starten und Projekt laden
rdm &
rc -J /pfad/zum/build
rc -J zeigt RTags auf das Verzeichnis mit compile_commands.json und
weist rdm an, das Projekt zu indizieren. Bei einer groĂen Codebasis
dauert das beim ersten Mal eine Weile; danach indiziert rdm nur noch
Dateien neu, die sich tatsÀchlich geÀndert haben.
Den Indizierungsfortschritt prĂŒfen:
rc --is-indexing
Die wichtigsten Abfragen
Alles Folgende setzt voraus, dass die Cursorposition als
datei:zeile:spalte angegeben wird â in der Praxis fĂŒllt eine
Editor-Integration das automatisch aus, aber es ist nĂŒtzlich, die
Rohform mindestens einmal gesehen zu haben.
Zur Definition des Symbols unter dem Cursor springen:
rc -f src/game.cpp:42:10
Jede Referenz auf ein Symbol finden â jede Stelle, an der es verwendet wird, nicht nur, wo es deklariert ist:
rc -r src/game.cpp:42:10
Referenzen anhand des Namens finden, ganz ohne Cursorposition:
rc -R moveItemsOneTile
Alle ĂŒberschriebenen/ĂŒberschreibenden virtuellen Methoden eines Symbols auflisten:
rc -w src/game.cpp:42:10
Jede Datei finden, die einen bestimmten Symbolnamen enthĂ€lt â nĂŒtzlich als erster, breiterer Durchgang:
rc --find-symbols moveItemsOneTile
Eine Datei anhand eines (Teil-)Namens im gesamten Projekt finden:
rc -F game.cpp
Der Unterschied zu rg -l 'moveItemsOneTile' src/ aus dem Ripgrep-Artikel
ist genau die Unterscheidung, die Regex nicht treffen kann: rc -r liefert
nur tatsĂ€chliche Referenzen, die ĂŒber das Typsystem aufgelöst wurden â
keinen Kommentar, der zufÀllig den Funktionsnamen erwÀhnt, kein
String-Literal, keine unabhĂ€ngige Ăberladung mit demselben Namen in einer
anderen Klasse.
Nach einer Ănderung neu indizieren
RTags indiziert eine Datei automatisch neu, sobald es die Ănderung bemerkt (es beobachtet die Dateien, die es kennt), aber nach einer gröĂeren Ănderung â etwa einer Header-Ănderung, die viele Ăbersetzungseinheiten betrifft â kann es helfen, das explizit anzustoĂen:
rc -e src/game.hpp
PrĂŒfen, ob eine bestimmte Datei im Index aktuell ist:
rc --dump-file-maps src/game.cpp
Editor-Integration
Am nĂŒtzlichsten ist RTags innerhalb eines Editors, wo datei:zeile:spalte
implizit ist â die Cursorposition ist die Abfrage. Das Projekt liefert
rtags.el fĂŒr Emacs mit, und rc ist einfach genug, um es auch aus
anderen Editoren heraus zu scripten, die eine Shell aufrufen und das
Ergebnis parsen können.
In Emacs, mit geladenem rtags.el, sind die alltÀglichen Bindings:
rtags-find-symbol-at-pointâ zur Definition springenrtags-find-references-at-pointâ alle Referenzen findenrtags-find-virtuals-at-pointâ Ăberschreibungen auflistenrtags-rename-symbolâ ein Symbol projektweit umbenennen, entlang tatsĂ€chlicher Referenzen statt TextĂŒbereinstimmungen
Letzteres ist besonders erwĂ€hnenswert: Ein Umbenennen ĂŒber rg/sed
muss raten, welche textuellen Vorkommen die richtigen sind, und trifft
bereitwillig ein unabhÀngiges Symbol, das zufÀllig denselben Namen trÀgt.
Ein Umbenennen ĂŒber RTags trifft nur das, was der Index als dasselbe
Symbol aufgelöst hat.
Wo RTags an seine Grenzen stöĂt
RTags braucht ein Projekt, das tatsĂ€chlich kompiliert â oder zumindest
eines, bei dem libclang jede Ăbersetzungseinheit weit genug parsen kann,
um Symbole aufzulösen. Eine halbfertige, gerade kaputte Datei degradiert
zu Teilergebnissen, aber nur fĂŒr diese eine Datei; der Rest des Index
bleibt unberĂŒhrt â trotzdem ein anderer Fehlermodus als bei ripgrep, dem
es völlig egal ist, ob der Code ĂŒberhaupt kompiliert.
AuĂerdem ist die Einrichtung aufwendiger: eine Compilation Database, ein
laufender Daemon und ein initialer Indizierungsdurchgang, gegenĂŒber rgs
konfigurationsfreier, sofort startender Textsuche. FĂŒr ein schnelles âkommt
dieser String irgendwo in diesem Repo vor" greife ich weiterhin eher zu
rg â RTags rechtfertigt seinen Aufwand erst, sobald die Frage lautet
âwas bedeutet dieses Symbol hier eigentlich".
Wie ich die beiden tatsÀchlich kombiniere
Mein Arbeitsablauf aus dem Ripgrep-Artikel setzt sich hier direkt fort.
Lizard meldet eine Funktion mit
hoher zyklomatischer KomplexitÀt; ich finde sie zuerst mit rg, da das
keinen Indizierungsschritt braucht und selbst bei einem Projekt, das ich
noch nicht angerĂŒhrt habe, sofort da ist:
rg -n -F 'moveItemsOneTile' src/
Sobald ich tatsÀchlich in der Funktion bin und ihre Aufrufer, ihre
Ăberschreibungen verstehen oder ein Umbenennen durchs ganze Projekt
verfolgen muss, wechsle ich zu RTags in Emacs. rg sagt mir, wo etwas
vorkommt; RTags sagt mir, was es ist und wo es tatsÀchlich verwendet
wird â zwei verschiedene Fragen, die zufĂ€llig beide damit beginnen,
diesen Namen im Code zu finden.
Fazit
Regex-basierte Suche und ein sprachbewusster Index lösen unterschiedliche
Probleme, die auf der Kommandozeile Àhnlich aussehen. rg ist schnell,
braucht keine Einrichtung und durchsucht Text â gut fĂŒr âich weiĂ noch
nicht genau, wonach ich suche". RTags braucht eine Compilation Database
und einen laufenden Daemon, antwortet dafĂŒr mit echtem semantischen
VerstĂ€ndnis von C/C++ â gut fĂŒr âich weiĂ genau, was dieses Symbol ist,
jetzt zeig mir jede Stelle, an der es wichtig ist". Zu wissen, welche
Frage man gerade eigentlich stellt, ist der gröĂte Teil der Entscheidung
zwischen den beiden.