Groovy in Vim

Raising the bar

Die Messlatte höher anzusetzen ist eines der Prinzipien im Software Crafting. Egal wie gut wir heute sind: wir können heute ein wenig dazu lernen und morgen ein wenig besser sein.

Für mich heißt das nicht konstanter Stress. Es reichen sehr kleine Schritte über lange Zeit. Das eigene Wohlergehen ist immer wichtiger als falsche Perfektion. Viele von uns sind aufgrund ihrer Neugier und Experimentierfreudigkeit zum Entwickeln gekommen. Diese intrinsische Motivation gilt es dabei zu nutzen. Mit Gewalt lernen bis zum Burnout ist das Gegenteil davon.

Typing is not the limit

Der limitierende Faktor beim Entwickeln sollte nicht die Tastatur sein. Der wahrscheinlich nie zu erreichende Idealzustand ist, dass der Gedanke den Code zu ändern sofort zu seiner Manipulation führt. Wir haben immer noch unsere Hände, die Tastatur, das Betriebssystem und die IDE zwischen unserem Gehirn und dem Code, aber Zehnfingerschreiben können wir alle lernen. Wir können die beste Tastatur für uns aussuchen, die Wiederholrate und Verzögerung im Betriebssystem auf unsere Bedürfnisse einstellen und unsere IDE möglichst ohne Maus bedienen lernen.

Für mich kam irgendwann vim oder ein vim-Plugin für IntelliJ dazu, um auch bei komplexeren Textmanipulationen möglichst wenig Zeit zu verlieren. Vims Modalität macht ihn für mich zu einem mächtigen Werkzeug bei der täglichen Arbeit. Ich habe Leute in Emacs oder Spacemacs ebenso schnell arbeiten sehen. Es geht mir hierbei nicht um den Editor, sondern das Entfernen von Schranken.

Beispiel

Wenn ich zu viel Zeit mit Textmanipulation verbringe, fange ich an, mich zu fragen, ob mir hier nicht ein vim-Feature fehlt, das ich noch nicht kenne. Meisten finde ich das Feature auch recht schnell, aber diese Woche ist es etwas anders gelaufen. Seht selbst:

Ich musste Daten aus Text extrahieren und individuelle SQL INSERTs daraus machen. Die Daten kamen aus einer ASCII-Art Tabelle:

| customer_number | other |
| --------------- | ----- |
| 101             | foo2  |
| 102             | foo3  |
| 103             | foo4  |
| 104             | foo5  |
| 105             | foo6  |
| 106             | foo7  |
| 107             | foo8  |
| 108             | foo9  |
| 109             | foo10 |
| 110             | foo11 |

Die anderen Daten habe ich nicht gebraucht, aber zu jeder customer_number musste ich das selbe SQL INSERT einmal schreiben. Alle anderen Daten waren gleich, nur die Nummer nicht:

INSERT INTO customer (customer_number, other VALUES (101, '...')

Was tun? Mein erster Gedanke war, über Block Selection , Ctrl-V und x in vim, alle customer_number-Zahlen heraus zu schneiden, und durch ein Makro ein SQL INSERT aus der Zeile zu machen.

Macros in vim funktionieren wie aufgezeichnete Kommandos. Man speichert sie in eines der Register, indem man anfängt mit q<Registername> ein Macro aufzuzeichnen. Die Aufzeichnung beendet man mit q wieder. Ins Macro kommt der “stumpfe” Teil der Arbeit, der wiederholt werden soll. Davor räume ich die ersten beiden Zeilen weg, schneide die Begrenzungen aus, bis nur noch die Zahlen auf einer Zeile übrig bleiben.

Als Teile des Macros verwende ich:

  • wir beginnen im Normal Mode
  • I zum Einfügen am Anfang der Zeile (→ Insert Mode)
  • Texteingabe im Insert Mode
  • <Esc> zum Verlassen des Insert Mode (→ Normal Mode)
  • A (Normal Mode) zum Einfügen am Ende der Zeile (→ Insert Mode)
  • <Esc> zum Verlassen des Insert Mode (→ Normal Mode)
  • j um eine Zeile nach unten zu gehen

Danach drücke ich q um die Aufzeichnung zu beenden. Wenn ich das Macro jetzt einmal mit @a laufen lasse, ändert vim für mich eine Zeile und springt zur nächsten Zeile. Letzteres ist wichtig, damit ich direkt das nächste Macro ausführen kann. Jetzt kann ich nämlich mit 8@a das Macro noch achtmal ausführen lassen und bin fertig.

Unten rechts sieht man die Kommandos im Normal Mode.

INSERTs über ein Macro generiert

Aber geht das nicht noch besser? Die Zahlen sind sortiert, es fehlen keine dazwischen und in Groovy würde ich einfach schreiben:

(101 .. 110).each { 
    println "INSERT INTO customer (customer_number, other) VALUES ($it, '...')" 
}

Also öffnete ich meine Groovy Shell und hatte das Problem noch schneller gelöst. Aber geht das nicht NOCH besser?

Was wäre, wenn ich den Groovy Code oben direkt in vim durch das Ergebnis der Evaluierung als Groovy-Programm ersetzen könnte. In Common Lisp habe ich das schon mal bei Rainer Joswig gesehen, als er in Emacs live Code evaluiert und an den REPL geschickt hat. In Clojure geht das auch, aber ich hätte das gerne in Groovy!

Nach etwas Googeln habe ich tommcdo/vim-express gefunden, das genau das erlaubt: Man verbindet eine Vim Motion mit einem selbst definierten Kommando, also “evaluiere das in Groovy”. Die Motion definiert den Text, der an den Evaluator übergeben wird, z.B. i" für den Inhalt von Klammern oder $ für “alles bis zum Zeilenende”.

Das Plugin habe ich mit vim-plug installiert:

call plug#begin('~/.config/nvim/plugged')

...

Plug 'tommcdo/vim-express'

...

call plug#end()

Und mein Kommando so definiert:

autocmd VimEnter * MapExpress GE system('groovy -e ' . shellescape(v:val))

Sobald vim fertig hochgefahren ist und alle Plugins geladen sind, wird mit MapExpress aus vim-express ein Kommando GE (ich dachte an “Groovy Evaluate”) definiert. Das Kommando führt die Funktion system() aus, die einen String zurückliefert und einen weiteren als auszuführendes Kommando annimmt. Das Ergebnis ersetzt dann den Text in der Motion. Den Parameter baue ich mir aus einem Groovy-Aufruf groovy -e und dem aktuellen Inhalt der Motion v:val zusammen, den ich vorher noch für meine Shell escapen muss.

Ergebnis:

Shout-outs

Danke an Sandra Parsick für den coolen Tip mit Terminalizer. Damit habe ich die animierten Terminal Sessions in diesem Blogpost gemacht.