EMACS:in alkeet osa 5 – TypeScript ja kumppanit

Edellisessä blogisarjan osassa opettelimme käyttämään terminaalia Emacsin sisältä. Sitä edeltävässä osassa puolestaan konfiguroimalla editoriin Python-ohjelmointityökalut. Tällä kertaa laajennamme Emacsimme toimintaamme entisestään asentamalla siihen työkalut TypeScriptin, JavaScriptin, TSX:n ja JSX:n editointiin. Näin huomaamme, miten joustavasti Emacs taipuu ympäristöksi eri ohjelmointikielillä työskentelyyn.

Alkuvalmistelut

Ennen kuin ryhdymme laajentamaan Emacsimme ominaisuuksia uusiin ohjelmointikieliin, otetaan käyttöön pari hyödyllistä pakettia: company ja exec-path-from-shell. Company on paketti automaattiseen tekstin täydentämiseen. Exec-path-from-shell puolestaan varmistaa, että Emacsin exec-path-muuttujaan ladataan polut suoraan editorin käynnistävän terminaalin PATH-ympäristömuuttujasta. Muuttuja exec-path sisältää listan poluista, joista Emacs voi etsiä aliprosesseissa suoritettavia ohjelmia. Sen ansiosta muun muassa myöhemmin hyödyntämämme tide-paketti löytää node-ohjelman samasta polusta kuin Emacsin käynnistänyt terminaalikin.

Company asentuu tällä hetkelläkin taustalla salaa Elpyn riippuvaisuutena, mutta se on hyvä konfiguroida eksplisiittisesti, jos joskus satumme poistamaan Elpyn ja siihen liittyvät asetukset. Se on niin kätevä apuväline, että voimme ottaa sen käyttöön kaikissa buffereissamme asettamalla päälle global-company-moden. Seurauksena Company ehdottaa täydennysvaihtoehtoja näppäillessämme tekstiä mihin tahansa bufferiin.

(use-package company
  :ensure t
  :init (global-company-mode))
Company-moden automaattinen tekstinsyöttö toiminnassa

Exec-path-from-shell puolestaan otetaan käyttöön seuraavasti:

(use-package exec-path-from-shell
  :ensure t
  :config
  (when (or (daemonp) (memq window-system '(ns x)))
    (exec-path-from-shell-initialize)))

Ehtolause konfiguraatio-osiossa tarkistaa, ajautuuko Emacs daemonina tai onko Emacs-ikkunan ikkunointijärjestelmänä joko X, GNUStep tai Macintosh Cocoa. Jos jompikumpi näistä käy toteen, PATH-arvot kopioidaan käynnistävästä terminaalista exec-pathiin. Käytännössä jälkimmäinen ehto käsittää graafiset ikkunat Linux- ja macOS-ympäristöissä. Tästä ehdosta kannattaa huomata, että memq-funktio tarkistaa, onko ensimmäinen argumentti jälkimmäisenä annettavan listan elementti. 

TypeScript hyökyy editoriin

TypeScript on JavaScriptin päälle rakentuva, Microsoftin kehittämä ohjelmointikieli, joka lisää JavaScriptiin staattisen tyyppitarkistuksen. Se on myös JavaScriptin ylijoukko eli kaikki JavaScript-koodi on myös validia TypeScriptiä. TypeScript on nykypäivänä suositeltavin tapa ohjelmoida JavaScriptiä vähänkään vakavammissa ohjelmistoprojekteissa.

Emacsille löytyy kevyt paketti nimeltä typescript-mode, mutta se tarjoaa pitkälti vain syntaksikorostuksen ja sisennyksen ohjelmointikielelle. Monipuolisempi IDE-paketti TypeScriptille on tide, joka sisältää myös ominaisuudet JavaScriptin, TSX:n ja JSX:n käsittelyyn.

Tide on siitä mukava paketti, että se sisältää hyvät asennus- ja konfigurointiohjeet. Kuten dokumentaatiosta näemme, voimme asentaa paketin seuraavasti:

(use-package tide
  :ensure t
  :after typescript-mode
  :hook ((typescript-mode . tide-setup)
         (typescript-mode . tide-hl-identifier-mode)
         (before-save . tide-format-before-save)))

Uutena use-packagen avainsanana kohtaamme tässä koodinpätkässä :afterin. Se varmistaa, että typescript-mode-paketti ladataan ennen kuin tide-paketti konfiguroidaan. Tide asentaa typescript-moden riippuvaisuutenaan.

Koodin hookit varmistavat, että tide-setup- ja tide-hl-identifier-mode-funktioita kutsutaan, kun typescript-mode aktivoituu. Tämä tapahtuu tavanomaisesti, kun TypeScript- eli .ts-tiedosto avataan bufferiin. Siinä missä funktio tide-setup luonnollisesti alustaa tiden, tide-hl-identifier-mode ottaa käyttöön koodissa esiintyvien nimien, kuten muuttujien, korostuksen kursorin liikkuessa niiden päälle.

Viimeinen koodirivi puolestaan varmistaa, että bufferi formatoidaan eli sen sisältämä koodi muotoillaan oikein aina ennen tallentamista. Tällä menettelyllä välttää koodatessa monet turhat muotoiluvirheet. 

Huomionarvoista on, että tide-format-before-save-funktio on fiksumpi kuin elpy-format-code, sillä se tarkistaa aina, että kyseessä on tide-bufferi, ennen kuin formatointi ajetaan. Siksi before-save-hookille ei tarvitse erikseen määritellä voimassaoloa vain lokaaliin bufferiin.

Kuten tiden pääsivulta käy ilmi, sen kanssa kannattaa ottaa käyttöön myös Flycheck-, company- ja ElDoc-paketit. Kaiken tämän on tarkoitus tapahtua typescript-moden astuessa käyttöön, muttemme halua toistaa samaa hookkia asetuksissamme tarpeettomasti. 

Luodaan ongelman välttämiseksi tiden käynnistysasetuksille oma funktionsa, kuten paketin dokumentaatiossa on myös tehty. Tällöin voimme käyttää vain yhtä typescript-mode-hookkia ja kutsua sen kohdalla tätä funktiota, joka asettaa kaikki tide-asetuksemme kohdalleen.

(defun setup-tide-mode ()
  "Set up tide mode and turn on related modes with tide specific configurations."
  (tide-setup)
  (tide-hl-identifier-mode 1))

Funktion ei tarvitse olla interaktiivinen, kuten tiden dokumentaatiossa, koska emme aio käyttää sitä omana komentonaan. Funktion alkuun on lisätty dokumentaatiomerkkijono, sillä Flycheck suosittelee, että jokaisella funktiolla on sellainen. Huomautettakoon myös, että tide-dokumentaatiossa käytetty +1 on ekvivalentti 1:n kanssa.

Nyt pärjäämme use-package-konfiguraatiossamme yhdellä hookilla:

(use-package tide
  :ensure t
  :after typescript-mode
  :hook 
  ((typescript-mode . setup-tide-mode)
   (before-save . tide-format-before-save))

Flycheckin asennus on jo sisällytetty init.eliimme. Varmistetaan, että se latautuu aina ennen tiden käyttöönottoa riippumatta tiden ja Flycheckin use-packagen konfiguraatioiden järjestyksestä koodissa. Vaikka init.elissä otamme käyttöön jo Flycheckin globaalissa tilassa, lisätään flycheck-moden päälle laittaminen myös tide-asetuksiimme siltä varalta, että joskus päätämme ottaa globaalin tilan pois päältä. Nyt koodi näyttää tältä:

(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))

 (use-package tide
  :ensure t
  :after (typescript-mode flycheck)
  :hook 
  ((typescript-mode . setup-tide-mode)
   (before-save . tide-format-before-save))

 

Flycheck tarkistaa nyt TypeScript-buffereista syntaksivirheet

Flycheck on hyvä asettaa syntaksitarkistamaan TypeScript-bufferi aina, kun flycheck-mode kytkeytyy päälle sekä tiedoston tallentamisen yhteydessä, kuten tiden dokumentaatiokin ehdottaa. Lisäksi kannattaa laittaa päälle myös idle-change-asetus, jolloin bufferi tarkistetaan aina pienen viiveen jälkeen viimeisimmästä muutoksesta.

(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))

Valitut syntaksitarkistuksen moodit kytketään siis päälle asettamalla lista näistä moodeista flycheck-check-syntax-automatically-muuttujan arvoksi.

Varmistetaan, että myös Company latautuu aina ennen tiden käyttöönottoa ja että se laitetaan tiden yhteydessä päälle riippumatta, onko global-company-mode päällä vai ei:

(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))
 (use-package tide
  :ensure t
  :after (typescript-mode flycheck company)
  :hook 
  ((typescript-mode . setup-tide-mode)
   (before-save . tide-format-before-save))

Tiden dokumentaatiosta näemme myös, että siinä on otettu käyttöön eldoc-mode. El Doc on Emacsin sisäänrakennettu dokumentaatiotyökalu, joka näyttää Emacs-ruudun alalaidassa muuttujadokumentaation tai funktion tyyppijalanjäljen, kun kursorin vie niiden päälle. El Doc on automaattisesti kytketty globaaliin moodiin, joten sen sisällyttäminen tide-konfiguraatioomme ei ole välttämätöntä. Voimme kuitenkin varmuuden vuoksi kytkeä sen päälle siltä varalta, että päätämme ottaa joskus globaalin moodin pois päältä.

Alimmaisimpana näemme El Docin näyttämän initMiddleware-funktion tyyppijalanjäljen

Tide-asetuksemme, joilla pääsee jo vauhtiin TypeScriptin kanssa, näyttävät nyt siis tältä:

(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)
  :hook ((typescript-mode . setup-tide-mode)
         (before-save . tide-format-before-save)))

Templaattitiedostot ja vaniljaviritykset

Pelkän TypeScriptin ohella olisi mukavaa, jos voimme React-koodausta harrastaessamme käyttää tideä myös TSX- ja JSX-buffereiden yhteydessä. JSX on Reactin oma JavaScriptin syntaksilaajennos, jota käyttämällä HTML:ää pystyy kirjoittamaan JavaScriptin sekaan. TSX on tämän laajennoksen TypeScript-versio. Näiden ohella olisi tietysti kaiken lisäksi hienoa, jos tide-pakettimme huomioisi myös perinteisen JavaScriptin!

JSX- ja TSX-tiedostoja varten otamme käyttöön web-mode-paketin, joka on paras vaihtoehto niiden kanssa työskentelyyn. Varmistamme myös, että tide ja flycheck ovat latautuneet ennen web-modea, jos koodin järjestys joskus vaihtuukin. Konfiguroidaan paketti siten, että jsx-tide ja tsx-tide lisätään sen käyttämiin syntaksitarkistimiin.

(use-package web-mode
  :ensure t
  :after (tide flycheck)
  :mode ("\\.jsx\\'" "\\.tsx\\'")
  :functions flycheck-add-mode
  :config
  (flycheck-add-mode 'jsx-tide 'web-mode)
  (flycheck-add-mode 'tsx-tide 'web-mode))

Avainsanalla :mode määritetään, että web-modea käytetään .jsx- ja .tsx-tiedostopäätteisten bufferien yhteydessä, koska ne eivät ole oletuksena web-moden tiedostomuotoja. Tiedostopäätteet on määritelty regexp-lausekkeena: \’ merkitsee merkkijonon päättymistä, ja \. pakenee pisteen, joka muuten olisi erikoismerkki. Lisäksi \ täytyy paeta kahdesti, sillä se on erikoismerkki sekä regexp-lausekkeissa että merkkijonoissa.

Kuten viime blogin osassa term-send-raw-string-funktion tapauksessa, flycheck-add-mode pitää ylläolevassa koodissa erikseen määritellä, jotta init.elimme ei aiheuta varoitusta. Tällä kertaa kuitenkin määrittely onnistuu :functions-avainsanaa käyttämällä, sillä Flycheck-paketti ladataan asetuksissamme. Voimme itse asiassa päästä eroon aiemmasta hieman kömpelöstä declare-function-määrittelystämmekin lataamalla term.el-paketin eksplisiittisesti require-komennolla:

(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-<" . multi-term-prev)
                  ("C->" . multi-term-next)
                  ("C-c C-x" . term-send-C-x)))))

Lisätään web-mode-määrittelyymme vielä hookki, joka käynnistää tiden, kun bufferin tiedostopäätteenä on joko .jsx tai .tsx.

(use-package web-mode
  :ensure t
  :after (tide flycheck)
  :functions flycheck-add-mode
  :mode ("\\.jsx\\'" "\\.tsx\\'")
  :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))

Tämä hookkimme aktivoituu aina, kun bufferin moodi on web-mode. Anonyymissa lambda-funktiossamme olemme käyttäneet let-lauseketta. Kyseiselle lausekkeelle annetaan ensimmäisenä argumenttina lista muuttujamäärittelyistä ja toisena argumenttina suoritettava lauseke, jossa näitä muuttujia hyödynnetään. 

Muuttujalistassa määrittelemme ext-muuttujan, jonka nimeksi annetaan tämänhetkisen bufferin tiedostopääte. Se saadaan sisäänrakennetulla file-name-extension-funktiolla Emacsin globaalista buffer-file-name-muuttujasta. Suoritettavassa lausekkeessa puolestaan tarkastamme string-equal-funktiolla, onko extin sisältämä merkkijono joko ”jsx” tai ”tsx”. Jos on, käynnistämme tiden.

Tide ja sen Flycheck-syntaksitarkistus toimivat nyt myös TSX-bufferissa.

JavaScriptiä varten sisäänrakennettua js-modea parempi Emacs-paketti on js2-mode. Sen voi asentaa helposti use-packagen avulla. Sen hookkina voimme käynnistää tiden. Lisäksi asetamme syntaksitarkistimeksi javascript-tiden. Varmistetaan lisäksi, että tide ja flycheck ovat latautuneet ensin ja että js2-mode käynnistyy JavaScript-tiedostojen yhteydessä. 

(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))

Tärkeää on huomata, että toimiakseen tiden JavaScript-syntaksitarkistus vaatii jsconfig.json-tiedoston projektin juureen.

Tideä voi nyt käyttää myös JavaScript-projekteissa.

Eslint, missä lienet

Todellista ohjelmistokehitystyötä varten meidän on syytä lisätä tide-konfiguraatioomme linttausvirheiden korostus. Lintteri on staattinen koodianalyysityökalu, joka ilmoittaa erilaisista ohjelmointivirheistä, bugeista, tyylivirheistä sekä epäilyttävistä rakenteista, joista kääntäjä ei piittaa. ESLint-lintteristä on vuosien kuluessa yleistynyt käytetyin ja suositelluin lintteri sekä TypeScriptin että JavaScriptin yhteydessä.

Linttausta varten meillä pitää olla ESLint asennettuna, minkä lisäksi ESLintiä vastaava Flycheck-syntaksitarkistin pitää ottaa käyttöön. Tavanomaisesti ESLint asennetaan erikseen kuhunkin TypeScript- tai JavaScript-projektiin Node-pakettimanagerilla komennolla:

npm install eslint --save-dev

Tällöin ESLint asentuu projektin node_modules-hakemiston alaisuuteen.

Jotta Flycheck tarkistaa myös linttauksen ESLinttiä hyödyntäen, meidän täytyy ottaa käyttöön javascript-eslint-niminen syntaksitarkistin. Valitettavasti Flycheck pystyy käyttämään kuitenkin vain yhtä tarkistinta kerrallaan. Tällä hetkellä sillä on käytössä TypeScript-bufferien yhteydessä typescript-tide-tarkistin, TSX-buffereiden yhteydessä typescript-tsx-tarkistin, JavaScript-bufferien yhteydessä javascript-tide-tarkistin ja JSX-buffereiden yhteydessä jsx-tide-tarkistin.

Flycheck tarjoaa kuitenkin mahdollisuuden ketjuttaa tarkistimia siten, että yhden tarkistimen ajauduttua seuraava vuorossa ajetaan aina. Jos siis ketjutamme javascript-eslint-tarkistimen yllä mainittujen tarkistimien kanssa, saamme Flycheckin tarkastamaan myös ESLint-virheet tide-bufferien yhteydessä. Lisätään tämä toiminnallisuus tide-konfiguraatioomme:

(use-package tide
  :ensure t
  :after (typescript-mode flycheck company)
  :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))

Tässä koodinpätkässä olemme myös ehkäisseet varoituksen, jonka flycheck-add-next-checker-funktion käyttö aiheuttaisi, huomioimalla sen use-packagen avainsanan :functions yhteydessä.

Jos nyt testaamme konfiguraatiotamme, se ei vielä toimi, ellei ESLint ole asennettu koneelle globaalisti. Tämä johtuu siitä, että javascript-eslint-tarkistin ei osaa automaattisesti etsiä ESLintin suoritettavaa tiedostoa projektin lokaalin node_modulesin alta. Sitä varten tarvitsemme pienen apuskriptin, jonka lisäämme flycheck-moden hookiksi:

(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))

Lisäämämme lambda-funktio saattaa äkkiseltään näyttää melko pelottavalta, mutta sen toiminnallisuus on yksinkertainen. Tiivistettynä se etsii lähimmän node_modules-hakemiston nykyisen bufferin sijainnista alkaen ja asettaa javascript-eslint-tarkistimen suoritettavaksi tiedostoksi sen alta löytyvän eslintin. 

Funktio käyttää hyväkseen let*-lauseketta, joka toimii kuten let-lauseke yhdellä erolla. Toisin kuin letin tapauksessa let* antaa käyttää jälkimmäisten muuttujien määrittelyssä hyväksi aiemmin samassa lausekkeessa määriteltyjä muuttujia. Siten voimme käyttää esimerkiksi muuttujaa current hyväksi muuttujan nmodules määrittelyssä.

Kohta kohdalta läpi käytynä lambda-funktio toimii seuraavasti:

  1. Määritellään muuttuja current. Sen arvoksi tulee nykyisen bufferin absoluuttinen tiedostopolku, tai jos buffer-file-name-muuttujan arvo on nil, bufferin oletushakemisto eli käytännössä sen absoluuttinen sijainti.
  2. Määritellään muuttuja nmodules. Sen arvoksi tulee locate-dominating-file-funktion palauttama hakemisto. Funktio locate-dominating-file lähtee liikkeelle current-muuttujan arvona annetusta hakemistosijainnista ylöspäin ja yrittää paikantaa ensimmäisen hakemiston, jonka alihakemistona on node_modules. Jos hakemistoa, jossa node_modules-sijaitsisi, ei hakemistopuusta löydy, nmodules saa arvon nil.
  3. Määritellään muuttuja eslint. Jos nmodulesin arvo ei ole nil, sen arvoksi tulee nmodulesin sisältämä polku laajennettuna node_modules/bin/eslint-päätteellä. Muuten eslint-muuttuja saa arvon nil.
  4. Jos eslint-muuttujan arvo ei ole nil ja jos Emacsilla on oikeus suorittaa sen sisältämä tiedostopolku, asetetaan Flycheckin javascript-eslint-tarkistimen suoritettavaksi tiedostoksi eslint-muuttujan arvo.
Muutoksiemme jälkeen ESLint-tarkistus toimii Flycheckin välityksellä tide-buffereissa.

Pikanäppäimet kohdalleen

Lisätään vielä tide-konfiguraatioomme muutama pikanäppäinyhdistelmä kaikkein useimmin käytetyille komennoille. Paketin valmiiksi määrittelemistä pikanäppäimistä M-. hyppää kursorin alla olevan symbolin määritelmään ja M-, palaa takaisin edeltävään hyppypisteeseen. Lisäämme seuraavalla koodinpätkällä omat pikanäppäimemme ja rajaamme ne tide-mode-mapin alaisuuteen, jolloin pikanäppäimet ovat käytössä vain tide-buffereissa:

(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))

Näiden toiminnallisuuksien nimet ovat valtaosin itsensä selittäviä, ja niiden selitykset löytyvät muun muassa paketin dokumentaatiosta. Kaiken kaikkiaan tide on kohtalaisen monipuolinen mutta helppokäyttöinen työkalu TypeScript- ja JavaScript-koodaukseen.

Lopullinen init.el-tiedostomme näyttää nyt tältä:

;;; 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)))))

(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 '(misterioso))
 '(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

Ensi blogisarjan osassa lisäämme puolestaan Emacsiimme joukon yleishyödyllisiä työkaluja, paketteja ja moodeja, jotka entisestään monipuolistavat sen toiminnallisuutta. Näin muovaamme editorista entistä käyttökelpoisemman esimerkiksi jokapäiväiseen web-ohjelmointiin.