Blogisarjan viidennessä osassa tuunasimme Emacsistamme Typescript- ja Javascript-editorin, joka osaa käsitellä myös TSX- ja JSX-formaatteja. Tällä kertaa keskitymme parantamaan editorin käytettävyyttä ja soveltuvuutta jokapäiväiseen ohjelmointiin.
Formaatit ojennukseen
Aloitetaan helposta ja lähdetään liikkeelle asentamalla Emacsiimme joukko paketteja, jotka tukevat muutamia web-ohjelmoinnissa yleisiä tiedostoformaatteja. Eräitä tavanomaisimpia dataformaatteja ovat XML, JSON ja YAML. Näistä XML tulee Emacsissa valmiiksi tuettuna nXML-moodin muodossa, jonka se osaa myös avata automaattisesti XML-buffereille tiedostopäätteen pohjalta. JSON- ja YAML-formaatteja varten on kuitenkin asennettava erilliset paketit, json-mode ja yaml-mode:
(use-package json-mode :ensure t) (use-package yaml-mode :ensure t)
Paketeista json-mode tuottaa JSON-tiedostoille paremman syntaksikorostuksen ja muutamia näppäinoikoteitä JSON:n editointiin. Näppäinoikotiet on dokumentoitu paketin sivuilla. YAML-buffereiden muokkaukseen tarkoitettu yaml-mode sisältää myös syntaksikorostuksen ja automatisoi sisennykset YAML-formaatin mukaisesti. Molemmat moodit aukeavat automaattisesti vastaavien tiedostomuotojensa buffereihin tiedostopäätteen pohjalta.

Erityisesti web-ohjelmistoprojekteissa Docker on yleistynyt kontitustyökaluna. Sen rinnalla käytetään usein puolestaan Docker Compose -ohjelmaa monesta Docker-kontista koostuvien sovellusten ajamiseen. Siksi jokapäiväistä koodaustyötämme helpottaa, jos Dockerfile- ja docker-compose-tiedostoja on mahdollisimman helppo editoida. Tätä varten asennamme dockerfile-mode– ja docker-compose-mode-paketit.
(use-package dockerfile-mode :ensure t) (use-package docker-compose-mode :ensure t)
Moodeista dockerfile-mode lisää syntaksikorostuksen Dockerfileille, kuin myös pikakomennot Docker-imagen kääntämiseen (engl. build). Docker Compose -tiedostot ovat puolestaan YAML-tiedostoja, mutta docker-compose-mode lisää automaattisen täydennyksen nimenomaan Docker Compose -formaatin mukaisille avainsanoille huomioiden myös formaatin version. Molemmat moodit osaavat tunnistaa niiden tiedostomuotoja vastaavat tiedostonimet: dockerfile-mode avautuu buffereihin, joiden tiedostonimi alkaa merkkijonolla Dockerfile, kun taas docker-compose-mode buffereihin, joiden tiedostonimi alkaa merkkijonolla docker-compose ja joilla on YAML-formaatin mukainen tiedostopääte.

Komentotyökalupakki
Seuraavaksi lisätään Emacs-konfiguraatioon joukko asetuksia, jotka parantavat editorin pikakomentoja ja niiden käytettävyyttä. Ensimmäisenä asennamme yhden hyödyllisimmistä paketeista, which-keyn, joka auttaa erityisesti aloittelijaa komentojen opettelussa ja kokeneempaakin niiden muistamisessa. Se näyttää puoliksi näppäiltyjen pikakomentojen pohjalta kaikki mahdolliset täydennysvaihtoehdot. Paketti asennetaan ja sen toiminnallisuus otetaan globaalisti käyttöön seuraavilla koodiriveillä:
(use-package which-key :ensure t :init (which-key-mode 1))

Hyödyllisiä apukomentoja voimme lisätä puolestaan konfiguraatioomme asentamalla crux-paketin. Crux tulee sanoista A Collection of Ridiculously Useful eXtensions for Emacs.
(use-package crux :ensure t)
Crux ei aseta mitään pikanäppäimiä automaattisesti vaan ne on asetettava manuaalisesti, koska kaikki komennot eivät ole hyödyllisiä kaikille käyttäjille. Meillekin monet komennot hoituvat tehokkaammin muiden pakettien välityksellä, ja osa on käytön kannalta niin triviaaleja tai harvinaisia, etteivät ne ansaitse omaa pikanäppäinyhdistelmäänsä. Alla on ehdotus muutamille hyödyllisille pikanäppäinasetuksille:
(use-package crux :ensure t :bind (("C-c t" . crux-transpose-windows) ("C-c d" . crux-delete-file-and-buffer) ("C-c r" . crux-rename-file-and-buffer) ("C-c o" . crux-kill-other-buffers) ("C-c i" . crux-find-user-init-file) ("C-c k" . crux-kill-whole-line) ("C-c DEL" . crux-kill-line-backwards) ("C-c s" . crux-sudo-edit)))
Uusien ikkunoiden luominen yhden Emacs-kehyksen sisälle onnistuu allekkain komennolla C-x 2 ja rinnakkain komennolla C-x 3. Ikkunan sulkeminen puolestaan tapahtuu komennolla C-x 0 ja kaikkien paitsi aktiivisen ikkunan sulkeminen komennolla C-x 1. Aktiivisen ikkunan vaihtaminen onnistuu taas näppäinyhdistelmällä C-x o. Nyt voimme ylläolevan koodin myötä myös vaihtaa ikkunoiden sisältämiä buffereita keskenään näppäinyhdistelmällä C-c t, joka aktivoi komennon crux-transpose-windows. Tämä flippaa aktiivisen ikkunan sisältämän ikkunan päittäin aina seuraavan ikkunan sisältämän bufferin kanssa.
Emacsissa nykyisen bufferin sulkeminen onnistuu komennolla C-x k ja tiedoston poistaminen esimerkiksi komennolla delete-file. Joskus tulee kuitenkin tarve poistaa sekä bufferi että sitä vastaava tiedosto samalla kertaa. Nyt näppäinyhdistelmä C-c d hoitaa tämän kutsuen komentoa crux-delete-file-and-buffer.

Vastaavasti Emacsissa on kaksi erillistä komentoa bufferin ja tiedoston uudelleen nimeämiselle, rename-buffer ja rename-file. Nyt näppäinyhdistelmästä C-c r aktivoituva crux-rename-file-and-buffer hoitaa nämä yhdellä kertaa.
Joskus editoria käyttäessä tulee tarve aloittaa puhtaalta pöydältä ja siivota se ylimääräisistä buffereista. Tätä varten pikanäppäin C-c o suorittaa komennon crux-kill-other-buffers, joka sulkee muut bufferit paitsi aktiivisen.

Helmin kanssa init.el on suhteellisen helppo avata nopeasti, sillä se on yleensä tallessa vähintään viimeisimmissä tiedostoissa. Näppäinyhdistelmä C-c i, joka aktivoi komennon crux-find-user-init-file tekee kuitenkin toimenpiteestä vaivattomamman.
Rivin leikkaaminen onnistuu Emacsissa näppäinyhdistelmällä C-k, joka poistaa ja kopioi rivin kursorin senhetkisestä sijainnista eteenpäin. Tätä kuitenkin helpottavat nyt näppäinyhdistelmät C-c k, jonka kutsuma crux-kill-whole-line leikkaa koko rivin riippumatta kursorin sijainnista, sekä C-c DEL, jonka kutsuma crux-kill-line-backwards leikkaa rivin kursorin senhetkisestä sijainnista taaksepäin. Huomaa, että DEL viittaa backspace-nappiin.
Viimeisenä komentona listassa on C-c s, joka aktivoi komennon crux-sudo-edit. Tämä avaa nykyisen bufferin sisältämän tiedoston pääkäyttäjänä. Se hyödyntää Emacsin sisältämää Tramp-työkalua, johon tutustumme myöhemmissä blogisarjan osissa. Pääkäyttäjänä tiedostojen avaaminen onnistuu suoraan Trampillakin, mutta se on huomattavasti tätä pikakomentoa kömpelömpää.

Elämänlaadunparannusta
Paremman formaatti- ja pikanäppäintuen ohella esittelemme lisäksi joukon paketteja, joilla jokapäiväistä työskentelyä on mahdollista sujuvoittaa. Ensimmäisenä lisäämme Emacsiin Visual Studio Code -tyylisen minimapin, joka näyttää aktiivisen bufferin pienoiskoossa Emacs-kehyksen laidassa ja helpottaa navigointia. Seuraavilla koodiriveillä asennamme minimapin, konfiguroimme pikakomennon sen päälle ja pois päältä kääntämiseksi sekä otamme sen oletuksena käyttöön:
(use-package minimap :ensure t :bind (("C-c m" . minimap-mode)) :init (minimap-mode 1))

Minimapin laitaa voi vaihtaa asettamalla muuttujan arvon minimap-window-location. Itse pidän enemmän minimapista oikeassa laidassa, jossa se häiritsee varsinaisen bufferin lukemista vähemmän. Siellä sen kanssa vaikuttaa syntyvän myös vähemmän ongelmia muiden Emacs-ikkunoiden kanssa. Asetan siis omassa init.elissäni arvon seuraavasti:
(use-package minimap :ensure t :bind (("C-c m" . minimap-mode)) :init (setq minimap-window-location 'right) (minimap-mode 1)) )
Minimapin asetuksia saa tarkemmin muokattua M-x customize-group-komennon kautta, valitsemalla listasta minimapin. Tällöin asetukset tallentuvat init.eliin custom-set-variables-komennon alle.
Ohjelmoidessa sulkujen ja muiden pareissa ilmaantuvien merkkien, kuten lainausmerkkien, käsittelyä helpottaa puolestaan huomattavasti smartparens-paketti. Se osaa muun muassa täydentää automaattisesti sulkeita ja navigoida niiden välillä huomattavasti paremmin kuin Emacsin oletusasetukset. Lisäksi se sisältää monia hyödyllisiä komentoja esimerkiksi koodin sulkeilla käärimiseen ja sulkujen poistamiseen. Erityisen tarpeellinen tämä paketti on sulkuja vilisevää Elisp-koodia editoidessa!
Asennetaan smartparens ja otetaan se sekä sen oletusasetukset käyttöön. Tiedosto smartparens-config.el sisältää toimivat oletusasetukset lukuisille ohjelmointikielille, mutta sitä ei ladata automaattisesti, joten se täytyy hakea erikseen require-komennolla.
(use-package smartparens :ensure t :init (require 'smartparens-config) :config (smartparens-global-mode t))
Asetetaan smartparensille muutamia pikanäppäinkomentoja käyttämällä sen sisäistä keymappia. Käytetään tällä kertaa M-p-alkuista prefiksiä, jotta smartparens-komennot on helpompi muistaa. Muistutuksena M viittaa meta-näppäimeen, joka on PC:illä oletuksena Windows-näppäin, Maceilla option-näppäin.
(use-package smartparens :ensure t :init (require 'smartparens-config) :bind (:map smartparens-mode-map ("M-p <right>" . sp-forward-sexp) ("M-p <left>" . sp-backward-sexp) ("M-p <down>" . sp-down-sexp) ("M-p <up>" . sp-backward-up-sexp) ("M-p s" . sp-splice-sexp) ("M-p x" . sp-kill-sexp)) :config (smartparens-global-mode t))
Näiden komentojen toiminnallisuus on seuraava:
- sp-forward-sexp: liikkuu seuraavaan sulkevaan merkkiin
- sp-backward-sexp: liikkuu edelliseen aukaisevaan merkkiin
- sp-down-sexp: liikkuu seuraavaan sisempään koodiblokkiin
- sp-backward-up-sexp: liikkuu edelliseen ulompaan koodiblokkiin
- sp-splice-sexp: poistaa merkkiparin sen hetkisen koodin ympäriltä
- sp-kill-sexp: poistaa koko merkkiparin ympäröimän koodiblokin
Lisäksi smartparens osaa täydentää suljeparin koodin ympärille, kun koodin maalaa ja näppäilee jonkin avaavista suljemerkeistä.

Koodin editointia helpottaa lisäksi Emacsin sisäänrakennettu HideShow-moodi. Sen pikanäppäimet ovat tosin tuskaisen hankalat, joten säädetään niitä hieman yksinkertaisemmiksi. Aktivoimme Hide-Show-moodin käyttöön Elisp-, Typescript- ja Python-buffereissa.
(use-package hideshow :bind (:map hs-minor-mode-map ("C-c +" . hs-show-block) ("C-c -" . hs-hide-block) ("C-c *" . hs-show-all) ("C-c /" . hs-hide-all)) :hook ((emacs-lisp-mode typescript-mode python-mode) . hs-minor-mode))
Tässä vaiheessa on hyvä eritellä major- ja minor-moodien käsitteet. Major-moodilla tarkoitetaan Emacsissa moodeja, jotka liittyvät tietynlaisen tekstin editoimiseen ja joita on buffereissa aina vain yksi, kuten typescript-mode tai python-mode. Minor-moodit ovat puolestaan lisäominaisuuksia tarjoavia moodeja, kuten hs-minor-mode tai smartparens-mode, joita aktivoidaan yleensä major-moodien hookkeina.

Yhteenveto
Tässä blogisarjan osassa olemme käyneet läpi yleishyödyllisiä moodeja, työkaluja ja pikanäppäinyhdistelmiä, joilla parannamme Emacsin käytettävyyttä koodieditorina. Seuraavassa blogitekstissä jatkamme tällaisten näppärien pakettien esittelemistä ja tutkimme, mihin kaikkeen Emacs venyykään!
Blogisarjassa työstettävää init.el-asetustiedostoa ylläpidetään nyt myös osoitteessa https://github.com/Buutti/emacs.
Tämänkertaisten muutosten jälkeen se näyttää seuraavalta:
;;; init.el --- Initialization file for Emacs ;;; Commentary: ;; Emacs Startup File (require 'package) ;;; Code: (add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/")) (unless package--initialized (package-initialize)) ;; inform byte compiler about free variables (eval-when-compile (defvar desktop-save) (defvar desktop-path) (defvar desktop-load-locked-desktop) (defvar desktop-auto-save-timeout)) ;; desktop-save settings (desktop-save-mode 1) (setq desktop-save t desktop-path '("~/.emacs.d/") desktop-load-locked-desktop t desktop-auto-save-timeout 5) ;; miscellaneous settings (add-to-list 'default-frame-alist '(fullscreen . maximized)) (add-hook 'before-save-hook 'delete-trailing-whitespace) (global-auto-revert-mode t) (setq column-number-mode t) (setq-default indent-tabs-mode nil) ;; automatic package installation (unless (package-installed-p 'use-package) (package-refresh-contents) (package-install 'use-package)) ;; auto updates (use-package auto-package-update :ensure t :config (setq auto-package-update-delete-old-versions t auto-package-update-interval 4) (auto-package-update-maybe)) ;; external package configurations (use-package exec-path-from-shell :ensure t :config (when (or (daemonp) (memq window-system '(mac ns x))) (exec-path-from-shell-initialize))) (use-package helm :ensure t :defines (helm-find-files-map helm-read-file-map) :bind (("M-x" . helm-M-x) ("C-x C-f" . helm-find-files) ("C-x b" . helm-mini) :map helm-map ("<tab>" . helm-execute-persistent-action) ("C-z" . helm-select-action) :map helm-find-files-map ("<DEL>" . helm-ff-delete-char-backward) ("C-<backspace>" . helm-find-files-up-one-level) :map helm-read-file-map ("<DEL>" . helm-ff-delete-char-backward) ("C-<backspace>" . helm-find-files-up-one-level)) :init (helm-mode 1)) (use-package magit :ensure t :bind (("C-x g" . magit-status))) (use-package elpy :ensure t :bind (:map elpy-mode-map ("C-<up>" . nil) ("C-<down>" . nil)) :hook (elpy-mode . (lambda () (add-hook 'before-save-hook 'elpy-format-code nil 'local))) :init (elpy-enable) :config (setq elpy-modules (delq 'elpy-module-flymake elpy-modules))) (use-package flycheck :ensure t :hook (flycheck-mode . (lambda() ;; Use ESLint from local node_modules (let* ((current (or buffer-file-name default-directory)) (nmodules (locate-dominating-file current "node_modules")) (eslint (and nmodules (expand-file-name "node_modules/.bin/eslint" nmodules)))) (when (and eslint (file-executable-p eslint)) (setq-local flycheck-javascript-eslint-executable eslint))))) :config (global-flycheck-mode)) (use-package company :ensure t :init (global-company-mode)) (defun setup-tide-mode () "Set up tide mode and turn on related modes with tide specific configurations." (tide-setup) (tide-hl-identifier-mode 1) (flycheck-mode 1) (setq flycheck-check-syntax-automatically '(save mode-enabled idle-change)) (company-mode 1) (eldoc-mode 1)) (use-package tide :ensure t :after (typescript-mode flycheck company) :bind (:map tide-mode-map ("C-x t s" . tide-restart-server) ("C-x t d" . tide-documentation-at-point) ("C-x t l" . tide-references) ("C-x t p" . tide-project-errors) ("C-x t e" . tide-error-at-point) ("C-x t n" . tide-rename-symbol) ("C-x t f" . tide-rename-file) ("C-x t o" . tide-format) ("C-x t x" . tide-fix) ("C-x t r" . tide-refactor) ("C-x t i" . tide-organize-imports) ("C-x t j" . tide-jsdoc-template) ("C-x t a" . tide-list-servers)) :hook ((typescript-mode . setup-tide-mode) (before-save . tide-format-before-save)) :functions flycheck-add-next-checker :config (flycheck-add-next-checker 'javascript-tide 'javascript-eslint) (flycheck-add-next-checker 'typescript-tide 'javascript-eslint) (flycheck-add-next-checker 'jsx-tide 'javascript-eslint) (flycheck-add-next-checker 'tsx-tide 'javascript-eslint)) (use-package web-mode :ensure t :after (tide flycheck) :mode ("\\.jsx\\'" "\\.tsx\\'") :functions flycheck-add-mode :hook (web-mode . (lambda () ;; Set up tide only if file extension is jsx or tsx (let ((ext (file-name-extension buffer-file-name))) (when (or (string-equal "jsx" ext) (string-equal "tsx" ext)) (setup-tide-mode))))) :config (flycheck-add-mode 'jsx-tide 'web-mode) (flycheck-add-mode 'tsx-tide 'web-mode)) (use-package js2-mode :ensure t :after (tide flycheck) :mode "\\.js\\'" :functions flycheck-add-mode :hook (js2-mode . setup-tide-mode) :config (flycheck-add-mode 'javascript-tide 'js2-mode)) (require 'term) (use-package multi-term :ensure t :bind (("C-x M-m" . multi-term) ("C-x M-o" . multi-term-dedicated-open) ("C-x M-t" . multi-term-dedicated-toggle) ("C-x M-s" . multi-term-dedicated-select) ("C-x M-c" . multi-term-dedicated-close)) :config ;; Change default multi-term shell to zsh if available (let ((zsh-bin (executable-find "zsh"))) (when zsh-bin (setq multi-term-program zsh-bin))) (defun term-send-C-x () (interactive) (term-send-raw-string "\C-x")) (setq term-bind-key-alist (append term-bind-key-alist '(("C-c C-j" . term-line-mode) ("C-c C-k" . term-char-mode) ("C-c C-x" . term-send-C-x) ("C-<" . multi-term-prev) ("C->" . multi-term-next))))) (use-package json-mode :ensure t) (use-package yaml-mode :ensure t) (use-package dockerfile-mode :ensure t) (use-package docker-compose-mode :ensure t) (use-package which-key :ensure t :init (which-key-mode 1)) (use-package crux :ensure t :bind (("C-c t" . crux-transpose-windows) ("C-c d" . crux-delete-file-and-buffer) ("C-c r" . crux-rename-file-and-buffer) ("C-c o" . crux-kill-other-buffers) ("C-c i" . crux-find-user-init-file) ("C-c k" . crux-kill-whole-line) ("C-c DEL" . crux-kill-line-backwards) ("C-c e" . crux-sudo-edit))) (use-package minimap :ensure t :bind (("C-c m" . minimap-mode)) :init (setq minimap-window-location 'right) (minimap-mode 1)) (use-package smartparens :ensure t :init (require 'smartparens-config) :bind (:map smartparens-mode-map ("M-p <right>" . sp-forward-sexp) ("M-p <left>" . sp-backward-sexp) ("M-p <down>" . sp-down-sexp) ("M-p <up>" . sp-backward-up-sexp) ("M-p s" . sp-splice-sexp) ("M-p x" . sp-kill-sexp)) :config (smartparens-global-mode t)) (use-package hideshow :bind (:map hs-minor-mode-map ("C-c +" . hs-show-block) ("C-c -" . hs-hide-block) ("C-c *" . hs-show-all) ("C-c /" . hs-hide-all)) :hook ((emacs-lisp-mode typescript-mode python-mode) . hs-minor-mode)) (custom-set-variables ;; custom-set-variables was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. '(custom-enabled-themes '(tango-dark)) '(elpy-rpc-python-command "python3") '(package-selected-packages '(flycheck elpy magit helm auto-package-update use-package)) '(python-shell-interpreter "python3")) (custom-set-faces ;; custom-set-faces was added by Custom. ;; If you edit it by hand, you could mess it up, so be careful. ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. ) (provide 'init) ;;; init.el ends here