Für die Entwicklung in Rust verwende ich Emacs und setze dafür die Modi rustic1 und lsp ein. Weiterhin sind die Language-Server rls oder rust-analyzer (externe Programme) notwendig, damit Emacs sich Informationen über den Code und die Syntax holen kann. Language-Server gibt es für verschiedene Sprachen (bis hin zu JSON und HTML/CSS), damit nicht jeder Editor (und jeder Major-Mode) das Analysieren der Programmiersprachen implementieren muss.

  1. Der Major-Mode rustic ist ein Abkömmling von rust-mode, der aber tendenziell wieder in rust-mode einfließen soll. 

Der Modus rustic bietet solch grundlegende Funktionen wie Einfärbung oder Einrückung des Codes, die bei der Programmierung helfen. Aber darüber hinaus bietet das Duo rustic/lsp noch viele Hilfen wie Vervollständigung, Zugriff auf die Dokumentation von Funktionen oder Navigation im Code. Neben Emacs habe ich aber immer noch mehrere XTerms geöffnet, um mit cargo den Code zu übersetzen und auszuführen.

Auf der Webseite von lsp gibt es:

Gute Unterstützung zu lsp bekommt man im Chat (auf Englisch, für Deutsch schreibt mir einfach):

Rustic und lsp

Language-Server installieren

Der Minor-Mode lsp ist ein allgemeiner Modus für verschiedene Sprachen, die einen extern Language-Server unterstützen. Damit muss nicht mehr im Emacs die Auswertung des Quelltexts implementiert sein, was mit den Jahren immer umfangreicher und komplizierter geworden ist. Language-Server können mit M-x lsp-install-server installiert werden, aber dies habe ich noch nicht ausprobiert. Ich habe den Language-Server direkt mit rustup installiert.

Für Rust gibt es gegenwärtig (Jan. 2021) zwei Language-Server: rls und die Weiterentwicklung rust-analyzer, wobei rust-analyzer wieder in rls 2.0 einfließen soll. Ich habe beide Language-Server installiert und nutze vorwiegend rust-analyzer, weil lsp-mode diesen bevorzugt auswählt. Zusätzlich benötigt man noch die Komponenten rust-src für die Beschreibungen der Funktionen der Standardbibliothek und rust-analysis1.

  1. Bei der Komponente bin ich mir nicht sicher, ob man die wirklich braucht 

% rustup component add rls rust-analysis rust-analyzer-preview rust-src

Für Cargo gibt es auch ein Paket ra_ap_rust-analyzer, aber mit diesem Programm kommt es bei mir gerade (2020-12-09, v0.0.27) zu Fehlern. Ich hoffe, dieses Wirrwarr löst sich irgendwann auf und man muss nur rls installieren.

Zu rls ist mit zu erwähnen, dass dieses oft nicht aktualisiert werden kann, weil die Entwicklung kontinuierlicher Anpassungen bedarf. Deshalb sieht man oft folgende Meldung. Daran also nicht stören, nach einigen Tagen geht es wieder.

% rustup update
info: syncing channel updates for 'nightly-x86_64-unknown-linux-gnu'
info: latest update on 2021-01-08, rust version 1.51.0-nightly (c8915eebe 2021-01-07)
info: skipping nightly which is missing installed component 'rls'

Rustic und lsp für Emacs einrichten

Im Emacs kann man mit M-x package-install die Pakete rustic und lsp-mode installieren. Zusätzlich ist das Paket flycheck1 nützlich, um während des Programmierens Fehler angezeigt zu bekommen.

  1. Die Konfiguration wird von lsp-mode automatisch vorgenommen, sobald das Paket installiert ist. 

Falls der Pfad zu den Cargo-Programmen nicht in PATH enthalten ist, kann man diesen in der init.el hinzufügen. Zusätzlich muss für Emacs noch die interne Liste exec-path angepasst werden:

(add-to-list 'exec-path (concat (getenv "HOME") "/.cargo/bin"))
(setenv "PATH" (concat (getenv "HOME") "/.cargo/bin:" (getenv "PATH")))

Da bei mir auch der Pfad zur Rust-Toolchain nicht in der allgemeinen Pfadliste steht, muss ich den Pfad zu rust-analyzer noch bekannt geben:

(with-eval-after-load 'rustic
  (setq rustic-analyzer-command
        '("/home/joerg/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rust-analyzer"))
  )

Verzögerter Start mit Desktop-save-mode

Wenn man sich mit desktop-save-mode beim Start von Emacs den letzten Zustand wiederherstellen lässt, kann mit lsp-mode der Start zu einer Compilier-Orgie führen. Daher ist es besser, die Initialisierung von lsp-mode auf das erste Öffnen des Puffers zu verlagern. Leider wird lsp-mode direkt von rustic geladen, weshalb man dieses zuerst unterbinden und dann händisch die verzögerte Initialisierung von lsp-mode starten muss:

; use lsp-deferred (in rustic-mode-hook) instead of lsp-mode
(setq rustic-lsp-client nil)

(add-hook 'rustic-mode-hook
          (lambda ()
            (rustic-lsp-mode-setup)
            ; defer start of lsp until the buffer is visited, prevent high load
            ; on Emacs startup due to desktop-save-mode
            (lsp-deferred)
            ))

flycheck nicht automatisch aktivieren

Diverse Untermodi bis hin zu desktop-save-mode aktivieren immer wieder flycheck, was meist einen Kompiliervorgang anstößt oder Fehler produziert. Daher habe ich diverse Stellen ausgemacht, an denen ich flycheck aus dem Automatismus herausgenommen habe und nur vom lsp-diagnostics-mode starten lasse; ggf. passiert dies verzögert, was sich positiv auf den Start von Emacs auswirkt.

(add-to-list 'desktop-minor-mode-handlers '(flycheck-mode . ignore))

(with-eval-after-load 'rustic-flycheck
  ; lsp-mode starts flycheck (if file is writable)
  (remove-hook 'rustic-mode-hook 'flycheck-mode)
  )

;; Don't start flycheck automaticly, but do so by the lsp-diagnostics-mode-hook
;; only for writable files
(with-eval-after-load 'lsp-diagnostics
  (setq lsp-diagnostics-provider :none)

  (add-hook 'lsp-diagnostics-mode-hook
            (lambda ()
              (unless buffer-read-only (lsp-diagnostics-flycheck-enable))
              ))
  )

Da sich schreibgeschützte Dateien eh nicht korrigieren lassen, bringt es auch nichts, wenn flycheck für diese Dateien Fehler meldet. Das Kompilieren der Systembibliotheken ist außerdem zu aufwändig, sollte man mal mit der Symbolverfolgung dorthin gelangen.

Systembibliotheken schreibgeschützt öffnen

Da ich mit der Navigation auch gern in die Quellen der Systembibliotheken springe, diese aber nicht verändern kann, sollen diese Puffer einen Schreibschutz erhalten.

(add-hook 'find-file-hook
          (lambda ()
            (when (string-match-p "/.\\(cargo\\|cache\\)/.*\\.rs\\'" buffer-file-name)
              (read-only-mode t)
              )
            ))

Integration von which-key

Mit dem Paket which-key bekommt man nach einer kurzen Verzögerung zum Anfang einer Tastenfolge die möglichen Fortsetzungen angezeigt.1 Für lsp-mode gibt es dafür noch zusätzliche Beschreibungen, die bei der Aktivierung initialisiert werden müssen:

  1. which-key muss in init.el global mit (which-key-mode t) aktiviert werden. 

(add-hook 'lsp-mode-hook #'lsp-enable-which-key-integration)

lsp-ui für Hinweisfenster

Das Paket lsp-ui zeigt nützliche Hinweise, wie die Dokumentation, weitere Verwendungsstellen in der Umgebung oder Verbesserungsvorschläge, in zusätzlichen Fenstern an. Wenn man mit dem Cursor oder der Maus auf einen Funktions- oder Variablennamen geht, zeigt es den Typ und andere Informationen dazu an.

Mir ist das allerdings zu unruhig und daher habe ich die Hilfe deaktiviert und mir stattdessen eine Funktion zum manuellen Ein- und Ausschalten des Hilfefensters gebaut. Die Verzögerung bis die Zusatzinformationen angezeigt werden, habe ich auch auf 1 Sekunde gesetzt, um weniger Ablenkung zu haben. Wie man noch mehr deaktivieren kann, ist in einer Anleitung beschrieben.

(setq lsp-ui-doc-delay 1)

(defun lsp-ui-doc-toggle (arg)
  "Show/hide documentation of the symbol at point

If ARG is unset, toggle the state. With ARG 0, hide the documentation,
else show it."
  (interactive "P")
  (if (if (null arg) (null (lsp-ui-doc--frame-visible-p)) arg)
      (lsp-ui-doc-show)
    (lsp-ui-doc-hide))
  )

(with-eval-after-load 'lsp-ui-doc
  ; disable doc popup, but toggle it by keys
  (setq lsp-ui-doc-show-with-cursor nil)
  (define-key lsp-ui-doc-mode-map "\C-cd" #'lsp-ui-doc-toggle)
  (define-key lsp-ui-doc-mode-map "\C-c\C-d" #'lsp-ui-doc-toggle)
  )

Für die xref-Befehle zur Navigation durch den Quelltext gibt es auch noch Ersetzungen, die reichhaltiger sind. Mit M-. bekommt man die Definitions- und Deklarationsstellen angezeigt, wenn es genau eine Stelle gibt, sonst springt man sofort dorthin; es gibt auch C-x 4 . und C-x 5 . für eine neue Anzeige bzw. ein neues Fenster. Zurück kommt man mit M-,. Mit M-? werden alle Stellen angezeigt, wo das aktuelle Symbol verwendet wird.

(with-eval-after-load 'lsp-ui
  (define-key lsp-ui-mode-map [remap xref-find-definitions] #'lsp-ui-peek-find-definitions)
  (define-key lsp-ui-mode-map [remap xref-find-references] #'lsp-ui-peek-find-references)
  )

Zusätzlich habe ich das Fenster noch im Aussehen angepasst, da es mir zu groß war.

(setq
 lsp-ui-doc-max-width 60
 lsp-ui-doc-max-height 18
 )

(with-eval-after-load 'lsp-ui-doc
  …
  (add-hook 'lsp-ui-doc-frame-hook
            (lambda (frame window)
              (set-face-attribute 'default frame :height 110)
              ))
  )

Folding mit Origami

Für den Überblick ist es oft hilfreich, einige Codestellen ausblenden zu können – engl. folding. Mit dem Paket lsp-origami wird dies auch unterstützt. Es gibt dann viele Befehle (M-x) zum Ein- und Ausblenden diverser Teile, die mit origami anfangen, aber am meisten verwende ich nur origami-close-all-nodes um alle möglichen Stellen auszublenden und origami-recursively-toggle-node um Code an der Cursor-Stelle ein- bzw. auszublenden.

Da ich das Ein-/Ausblenden so häufig nutze, habe ich es an die Taste Shift+Tab gebunden:

(with-eval-after-load 'lsp-mode
  ; folding
  (add-hook 'lsp-after-open-hook #'lsp-origami-try-enable)
  (define-key lsp-mode-map (kbd "<backtab>") #'origami-recursively-toggle-node)
  …
  )

Language-Server aus der Modeline entfernen

Mich stört in der Modeline der Name des Language-Servers und mir genügt völlig die Kennung LSP. Mit dem Paket diminish kann man den Eintrag entsprechend einkürzen:

(with-eval-after-load 'lsp-mode
  ; remove the lsp-server from the minor mode indicator in the mode line and
  ; only show "LSP"
  (diminish 'lsp-mode "LSP")
  …
  )

Diverse Anzeigen abschalten

Mit all den Funktionen, die lsp-mode bietet, wird jede Cursorbewegung zum Leuchtfeuerwerk, weil ständig irgendwelche Fehlermeldungen und Vorschläge angezeigt werden. Mich nervt das und ich will meine Ruhe beim Programmieren. Deshalb habe ich mithilfe der Anleitung zum Deaktivieren einiger Funktionen mir die Code-Aktionen am rechten Rand und Funktionsdeklarationen in der Fußzeile deaktiviert.

(setq
 lsp-ui-sideline-show-code-actions nil

 lsp-signature-render-documentation nil
 )

Definitions- und Nutzungsstellen finden

Die Stelle, an der eine Funktion oder Variable definiert ist, kann in vielen Programmiermodi mit M-. (xref-find-definitions) angesprungen werden. Zurück kommt man mit M-, (xref-pop-marker-stack). Die Stellen, an der eine Funktion oder Variable genutzt wird, können mit M-? (xref-find-references) angezeigt werden.

Dies Funtioniert auch mit lsp und lsp bietet auch spezielle Funktionen, die die Definitions- und Nutzungsstellen genauer anzeigen. Hierfür kann man die oben erwähnten Tastenkombinationen in der init.el umdefinieren:

(with-eval-after-load 'lsp-ui
  (define-key lsp-ui-mode-map [remap xref-find-definitions] #'lsp-ui-peek-find-definitions)
  (define-key lsp-ui-mode-map [remap xref-find-references] #'lsp-ui-peek-find-references)
  )

Hervorhebung von Nutzungsstellen

Hilfreich bei der Programmierung ist auch die Hervorhebung aller Stellen auf dem Bildschirm, an denen eine Variable oder Funktion genutzt wird. Dazu muss man nur kurz an einer Stelle verweilen und die anderen Stellen leuchten auf.

Variablen umbenennen

Sehr praktisch zum Überarbeiten des Codes ist die Umbenennung von Variablen, Funktionen oder Datentypen inklusive aller Nutzungsstellen mit lsp-rename. Dafür einfach den Cursor auf dem Namen platzieren und M-x lsp-rename aufrufen. Die Änderungen erfolgen auch dateiübergreifend.

Expansion von Makros

Eine Stärke von Rust sind die Makros. Jedoch ist bei mancher Compilermeldung zu einem Fehler nicht klar erkenntlich, was das Problem ist. Mit dem Paket cargo-expand kann man sich auf der Kommandozeile den kompletten Quelltext eines Programme, dessen Makros ausgelöst wurden, ansehen. Innerhalb von Emacs kann man aber auch mit dem Cursor über einem Makro einfach M-x lsp-rust-analyzer-expand-macro aufrufen und bekommt das Ergebnis dieses eines Makros angezeigt.

Leider ist im Rustic gegenwärtig (Jan. 2021) ein Fehler, weshalb man in der init.el folgende Anpassung treffen muss:

(with-eval-after-load 'rustic
  ; https://github.com/brotzeit/rustic/issues/211
  (setq lsp-rust-analyzer-macro-expansion-method 'lsp-rust-analyzer-macro-expansion-default)
  )

Vervollständigungen

Grundsätzlich kann ich für Vervollständigungen das Emacs-Paket yasnippet empfehlen. Ich habe mir damit auch für Rust einige nette Helferlein gebastelt bis hin zu großen Codeschnipseln mit den Anweisungen für clap. Mit der Funktion yas-new-snippet geht dies sehr einfach:

p ⇒ println!("${1:{}}", $0);
s ⇒ Some($1)$0
ok ⇒ Ok(${1:()})$0

Da Rust mit Typen arbeitet, ist fast überall immer bekannt, welche Methoden ein Objekt besitzt. Hierfür habe ich das Emacs-Paket company installiert, womit sich nach einer kurzen Verzögerung ein kleines Fenster unter dem Cursor öffnet und die möglichen Vervollständigungen anzeigt.

Magische Vervollständigungen

Rust-analyzer bietet einige besondere Vervollständigungen für Ausdrücke, die keine echten Funktionen sind, sondern den Ausdruck umwandeln. Praktisch ist zum Beispiel an einen Wert .dbg anzuhängen und mit Tab die Umwandlung auszulösen. Dies führt zu dbg!(…).

Mit expr.if erhält man if expr {} oder if let … {}, wenn der Ausdruck ein Option oder Result ist. Im zweiten Fall kann man auch mit .match sich die entsprechende Anweisung match … { … => } generieren lassen.

Code-Aktionen

Neben den Vervollständigungen kann man auch an bestimmten Punkten Aktionen wie das Einfügen von Vorlagen für die Implementation von Traits, das Einfügen von use-Anweisungen bei bekannten Typen, das Umwandeln zwischen if let und match oder Code/Anweisungen in Module/Funktionen/Variablen überführen. Man muss nur an der passenden Stelle die Funktion lsp-execute-code-action aufrufen; wer das Emacs-Paket helm nutzt, kann auch helm-lsp-code-actions verwenden. Wo sich diese Stellen befinden, ist in der Dokumentation sehr gut dargestellt. Ob an einer Stelle eine Aktion verfügbar ist, sieht man an der Glühbirne am Ende der Modeline.

Wer in der Konfiguration nicht lsp-ui-sideline-show-code-actions abgeschaltet hat, bekommt die Möglichkeiten immer am rechten Rand angezeigt. Aber mir ist die Anzeige damit zu unruhig. Die Glühbirne genügt mir.

Cargo-Befehle aufrufen

Mit restic kommen auch eine Reihe an Funktionen für den Aufruf von cargo-Befehlen. Nützlich ist zum Beispiel rustic-cargo-add, um crates zum Projekt hinzuzufügen, oder rustic-cargo-outdated, um nach neueren Versionen zu suchen. (Das Emacs-Paket cargo ist dafür nicht notwendig.)

Spielwiese zum Experimentieren

Wer play.rust-lang.org kennt, wird das Paket rust-playground lieben. Damit lässt sich sehr leicht ein neues Projekt erstellen, mit C-c C-c kompilieren und mit C-c k wieder löschen. Ideal, um mal einen Codeschnippsel auszuprobieren oder zu testen, was der Rückgabewert einer Funktion bei einem bestimmten Wert ist.

Aktuell (Jan. 2021) ist leider in dem Projekt ein Fehler, der sich aber leicht beheben lässt. Danach das Compilieren der veränderten Datei mit M-x byte-compile-file nicht vergessen, sonst wird weiterhin die Original-elc-Datei verwendet.

Rust-mode und Racer

Anfangs habe ich rust-mode und racer genutzt, weil ich rls, den Lanugage-Server für Rust, nicht zum Laufen bekommen hatte. Mittlerweile klappt es und ich bin umgestiegen, weil lsp mehr Funktionen wie das Umbenennen von Bezeichnern bietet. Durch die Installation beider Modi kam es bei mir anfangs zu unschönen Interferenzen, die aber durch den Wechsel bedingt waren. Wer nur einen Modus installiert hat, sollte keine Probleme haben bzw. sollte der Bugfix irgendwann in lsp einfließen.

Die folgende Anleitung ist auch etwas veraltet (Stand: Jan. 2021), sollte aber dennoch funktionieren. Ich nutze nur noch rustic und rust-analyzer.

Zum regulären Major-Mode rust gibt es den Minor-Mode racer, der die Definition von Funktionen angezeigt, wenn man mit dem Cursor auf einem Funktionsnamen verweilt, oder mit dem man zur Definition einer Funktion mit M-. springen; mit M-, kann man zurück springen. Mit racer-describe kann man sich die Hilfe zu einer Funktion anzeigen lassen; dies wird unten an C-c d bzw. C-c C-d gebunden. Durch die Einbindung des Minor-Modes company bekommt man sogar Tab-Vervollständigung.

Siehe auch: Installationsanleitung von emacs-racer; vor yasnippet v0.13 ist Yasnippet und Company gemeinsam -nutzen notwendig.

Rust-Mode mit Rustic deaktivieren

Wenn beide Modi installiert sind, kann es passieren, dass rust-mode statt rustic für einige Dateien geladen wird. Da sich rust-mode selbst in die Liste für die Dateierweiterungen einträgt, muss man dies nach dem Laden rückgängig machen:

(with-eval-after-load 'rust-mode
  ; remove rust-mode from the list of file extension mappings
  (setq auto-mode-alist (rassq-delete-all 'rust-mode auto-mode-alist))
  )

Da auch andere Modi (bis hin zu markdown-mode) rust-mode aktivieren, habe ich zeitweise auch gezielt einen Fehler herbeigeführt, um all die Stellen zu finden, die auf rust-mode verweisen. Allerdings ist diese mit Vorsicht zu genießen, da die Fehler an allen möglichen Stellen (auf beim Start) auftreten können:

(add-hook 'rust-mode-hook
          (lambda () (error "Don't use rust-mode, use rustic")

Fehlerkennzeichnung mit flyCheck

Mit flyCheck kann man sich direkt im Quelltext die Fehler hervorheben, mit C-c ! l die Fehler anzeigen und mit C-c ! p bzw. C-c ! n rückwärts oder vorwärts springen. Mehr Informationen gibt es in der Kurzanleitung.

Unter Debian müssen die Pakete elpa-flycheck und flycheck-doc installiert werden und über die Emacs-Paketverwaltung die Anpassung an Rust (M-x package-install flycheck-rust).

Anpassung in init.el:

(add-hook 'rust-mode-hook #'flycheck-mode)
(add-hook 'rust-mode-hook #'flycheck-rust-setup)

Modus für cargo

Es gibt einen Minor-Mode, um die Befehle von cargo aus Emacs heraus aufrufen zu können. Installation mit M-x package-install cargo und in init.el mit (add-hook 'rust-mode-hook #'cargo-minor-mode) aktivieren.