שבוע ראשון עם אימקס

פוסט זה כולל טיפ קצר על כלי עבודה בסביבת Linux. בשביל ללמוד יותר על עבודה בסביבת Linux ו Unix אני ממליץ לכם לבדוק את קורס Linux שיש לנו כאן באתר. הקורס כולל מעל 50 שיעורי וידאו והמון תרגול מעשי ומתאים גם למתחילים.
 

בשבועות האחרונים התחלתי לעבוד עם emacs אחרי שנים של עבודה ב vim, בעיקר בשביל לנסות סביבות נוספות ולראות מה דומה או שונה. כמו כל מעבר ההתחלה קשה ולאט לאט זה הופך קל יותר. אם גם אתם מחפשים סביבת עבודה חדשה הנה כמה מחשבות שאולי יעזרו לכם להתחיל במעבר.

1. סביבת עבודה מול עורך טקסט

Emacs הוא הרבה יותר סביבת פיתוח מאשר עורך טקסט (או כמו הבדיחה הישנה ״אימקס היא מערכת הפעלה מצוינת שהדבר היחיד שחסר בה זה עורך טקסט נורמלי״). זמן העליה יותר ארוך, הניווט לקבצים מחוץ לפרויקט קצת פחות נוח. ההבדלים קטנים: למשל כשאני פותח קובץ עם כפתור ימני ו״פתח באמצעות״ הקובץ ייפתח בחלון האימקס הפעיל ולא בחלון חדש. או שפתיחת קובץ משנה את התיקייה הנוכחית להיות התיקיה של הקובץ כך שכשתפתחו קובץ נוסף אימקס יחפש אותו במיקום יחסי למיקום הקובץ שכרגע פתוח.

יש לזה גם יתרונות כמובן: השלמות אוטומטיות עובדות טוב יותר, יש תמיכה בפקודות אסינכרוניות כך שבדיקת תקינות קוד עובדת חלק יותר וגם הפעלת git. בסך הכל אחרי שמתרגלים אפשר לחיות עם זה.

2. הרחבות

התקנת חבילות באימקס מוטמעת בתוך העורך ואפשר להפעיל את כלי ניהול החבילות המשולב באמצעות הפקודה:

list-packages

כדי להכנס למצב הכנסת פקודה אגב אתם מקלידים Meta-x. על מק זה להחזיק את כפתור Alt וללחוץ על x. לאחר התקנת חבילה יש לטעון אותה דרך קובץ האתחול. הקובץ נקרא init.el ונמצא בתיקיית הבית בנתיב:

$HOME/.emacs.d/init.el

כך נראה קובץ ההגדרות הנוכחי שלי, ולאחריו רשימת התוספים החשובים בהם אני משתמש:

(when (>= emacs-major-version 24)
  (require 'package)
  (add-to-list
   'package-archives
   '("melpa" . "http://melpa.org/packages/")
   t)
  (package-initialize))

(setq linum-format "%d ")


;; hooks
(add-hook 'after-init-hook 'global-company-mode)
(add-hook 'ruby-mode-hook 'robe-mode)
(setq ruby-deep-indent-paren nil)

;; rvm
(require 'rvm)
(rvm-use-default)





;; company mode

; ruby
(eval-after-load 'company
  '(push 'company-robe company-backends))

; tern (javascript)
(eval-after-load 'company
  '(push 'company-tern company-backends))

; python
(defun my/python-mode-hook ()
  (add-to-list 'company-backends 'company-jedi))
(add-hook 'python-mode-hook 'my/python-mode-hook)




;; Use regex searches by default.
(global-set-key (kbd "C-s") 'isearch-forward-regexp)
(global-set-key (kbd "\C-r") 'isearch-backward-regexp)
(global-set-key (kbd "C-M-s") 'isearch-forward)
(global-set-key (kbd "C-M-r") 'isearch-backward)

(global-set-key (kbd "C-x C-o") 'company-complete)

(setq-default indent-tabs-mode nil)
(setq standard-indent 2)
(setq make-backup-files nil) 

(add-to-list 'auto-mode-alist '("\\.jsx\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.erb\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.html?\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.js?\\'" . web-mode))

(add-hook 'web-mode-hook (lambda () (tern-mode t)))


;; evil mode
(setq evil-want-C-u-scroll t)
(global-evil-surround-mode 1)
(global-evil-leader-mode 1)
(global-evil-matchit-mode 1)
(evil-commentary-mode 1)

(evil-mode 1)


;; other modes
(yas-global-mode 1)
(electric-pair-mode 1)
(global-linum-mode 1)



(evil-global-set-key 'normal (kbd "C-p") 'fiplr-find-file)

(setq fiplr-ignored-globs '((directories (".git" ".svn"))
                            (files ("*.jpg" "*.png" "*.zip" "*~" "*.cache" "*.keep"))))

(setq web-mode-enable-auto-closing t)
(setq web-mode-enable-auto-pairing t)

(evil-leader/set-key "n" 'neotree-toggle)

(load-theme 'tango-dark)




;; flycheck
(global-flycheck-mode)
(flycheck-define-checker jsxhint-checker
  "A JSX syntax and style checker based on JSXHint."

  :command ("jsxhint" source)
  :error-patterns
  ((error line-start (1+ nonl) ": line " line ", col " column ", " (message) line-end))
  :modes (web-mode))

(add-hook 'web-mode-hook
          (lambda ()
            (when (equal web-mode-content-type "jsx")
              ;; enable flycheck
              (flycheck-select-checker 'jsxhint-checker)
              (flycheck-mode))))



;;; file name expansion
(fset 'my-complete-file-name
      (make-hippie-expand-function '(try-complete-file-name-partially
                                     try-complete-file-name)))




(defun my-hippie-expand-completions (&optional hippie-expand-function)
  "Return the full list of possible completions generated by `hippie-expand'.
    The optional argument can be generated with `make-hippie-expand-function'."
  (let ((this-command 'my-hippie-expand-completions)
        (last-command last-command)
        (buffer-modified (buffer-modified-p))
        (hippie-expand-function (or hippie-expand-function 'hippie-expand)))
    (flet ((ding)) ; avoid the (ding) when hippie-expand exhausts its options.
      (while (progn
               (funcall hippie-expand-function nil)
               (setq last-command 'my-hippie-expand-completions)
               (not (equal he-num -1)))))
    ;; Evaluating the completions modifies the buffer, however we will finish
    ;; up in the same state that we began.
    (set-buffer-modified-p buffer-modified)
    ;; Provide the options in the order in which they are normally generated.
    (delete he-search-string (reverse he-tried-table))))

(defmacro my-ido-hippie-expand-with (hippie-expand-function)
  "Generate an interactively-callable function that offers ido-based completion
    using the specified hippie-expand function."
  `(call-interactively
    (lambda (&optional selection)
      (interactive
       (let ((options (my-hippie-expand-completions ,hippie-expand-function)))
         (if options
             (list (ido-completing-read "Completions: " options)))))
      (if selection
          (he-substitute-string selection t)
        (message "No expansion found")))))

(defun my-ido-hippie-expand ()
  "Offer ido-based completion for the word at point."
  (interactive)
  (my-ido-hippie-expand-with 'hippie-expand))

(defun my-ido-hippie-expand-filename ()
  "Offer ido-based completion for the filename at point."
  (interactive)
  (my-ido-hippie-expand-with
          (make-hippie-expand-function '(try-complete-file-name))))

;; (global-set-key (kbd "C-c C-f") 'my-ido-hippie-expand-filename)
(evil-global-set-key 'insert (kbd "C-x C-f") 'my-ido-hippie-expand-filename)


;;; text resize fix:
;;; http://unix.stackexchange.com/questions/29786/font-size-issues-with-emacs-in-linum-mode/30087#30087

;; This script is set for a `text-scale-mode-step` of `1.04`
(setq text-scale-mode-step 1.04)
;;
;; List: `Sub-Zoom Font Heights per text-scale-mode-step`  
;;   eg.  For a default font-height of 120 just remove the leading `160 150 140 130` 
(defvar sub-zoom-ht (list 160 150 140 130 120 120 110 100 100  90  80  80  80  80  70  70  60  60  50  50  50  40  40  40  30  20  20  20  20  20  20  10  10  10  10  10  10  10  10  10  10   5   5   5   5   5   2   2   2   2   2   2   2   2   1   1   1   1   1   1   1   1   1   1   1   1))
(defvar sub-zoom-len (safe-length sub-zoom-ht))
(defvar def-zoom-ht (car sub-zoom-ht))
(set-face-attribute 'default nil :height def-zoom-ht)

(defun text-scale-adjust-zAp ()
   (interactive)
   (text-scale-adjust 0)
   (set-face-attribute 'linum nil :height def-zoom-ht)
 )
(global-set-key [C-kp-multiply] 'text-scale-adjust-zAp)

(defun text-scale-decrease-zAp ()
   (interactive)
   (if (not (boundp 'text-scale-mode-amount)) ;; first-time init  
              (setq  text-scale-mode-amount 0))
   (setq text-scale (round (/ (* 1 text-scale-mode-amount) 
                                   text-scale-mode-step)))
   (if (> text-scale (- 1 sub-zoom-len))
       (progn
         (text-scale-decrease text-scale-mode-step)
         (if (<= 0 text-scale-mode-amount)
             (set-face-attribute 'linum nil :height def-zoom-ht)
           (if (> 0 text-scale-mode-amount)
               (set-face-attribute 'linum nil :height 
                                     (elt sub-zoom-ht (- 0 text-scale)))))))
)
(global-set-key [C-kp-subtract] 'text-scale-decrease-zAp)

(defun text-scale-increase-zAp ()
   (interactive)
   (if (not (boundp 'text-scale-mode-amount)) ;; first-time init  
              (setq  text-scale-mode-amount 0))
   (setq text-scale (round (/ (* 1 text-scale-mode-amount) 
                                   text-scale-mode-step)))
   (if (< text-scale 85)
       (progn
         (text-scale-increase text-scale-mode-step)
         (if (< (- 0 text-scale-mode-step) text-scale-mode-amount)
             (set-face-attribute 'linum nil :height def-zoom-ht)
           (if (> 0 text-scale-mode-amount)
               (set-face-attribute 'linum nil :height 
                                     (elt sub-zoom-ht (- 0 text-scale)))))))
)
(global-set-key [C-kp-add] 'text-scale-increase-zAp)


;; Zoom font via Numeric Keypad
(global-set-key [C-kp-multiply] 'text-scale-adjust-zAp)
(global-set-key [C-kp-subtract] 'text-scale-decrease-zAp)
(global-set-key [C-kp-add]      'text-scale-increase-zAp)

;; Zoomf font via Control Mouse Wheel
(global-set-key (kbd "<C-mouse-4>") 'text-scale-increase-zAp)
(global-set-key (kbd "<C-mouse-5>") 'text-scale-decrease-zAp)





;; evil args

;; locate and load the package
(add-to-list 'load-path "path/to/evil-args")
(require 'evil-args)

;; bind evil-args text objects
(define-key evil-inner-text-objects-map "a" 'evil-inner-arg)
(define-key evil-outer-text-objects-map "a" 'evil-outer-arg)

;; bind evil-forward/backward-args
(define-key evil-normal-state-map "L" 'evil-forward-arg)
(define-key evil-normal-state-map "H" 'evil-backward-arg)
(define-key evil-motion-state-map "L" 'evil-forward-arg)
(define-key evil-motion-state-map "H" 'evil-backward-arg)

;; bind evil-jump-out-args
(define-key evil-normal-state-map "K" 'evil-jump-out-args)

 

3. תוסף evil-mode

קישור: http://www.emacswiki.org/emacs/Evil

התוסף הראשון שהתקנתי נקרא evil-mode והוא מעין מדמה vim, כך שכפתורי התנועה שאני רגיל אליהם משם עובדים. רוב הדברים עובדים כמו שציפיתי, מלמד השלמות פרמטרים ב Command Mode. כך למשל במקום לכתוב:

:cd <tab>

צריך לכתוב:

:cd<enter>

ורק אחרי להתחיל לכתוב שם תיקייה עם השלמות. מלבד זאת יש עולם שלם של תוספים ל evil, כך שרוב הזמן אפשר להרגיש בבית.

4. תוסף web-mode

קישור: http://web-mode.org/

תוסף מעולה למפתחי ווב: הוא כולל הגדרות אינדנטציה ותחביר עבור כל שפת Templates שאתם מכירים, כולל ריאקט ו JSX. ניווט מהיר בין הקבצים כולל קבצי unit tests, השלמה אוטומטית של תגיות HTML ומאפייני CSS ויכולות רבות נוספות.

5. תוסף evil-rails

קישור: https://github.com/antono/evil-rails

התוסף מציע ניווט מהיר בין קבצים בפרויקט ריילס על פי ציון סוג הקובץ עם השלמות, והוא מעין גירסא של vim-rails לאימקס. כך למשל בשביל לנווט למודל תוכלו להקליד:

:Rmodel<enter>

ולאחר מכן שם המודל. זה שונה מההתנהגות של וים שם ההשלמה היתה כבר באותה השורה.

6. תוסף company-mode

קישור: http://company-mode.github.io/

יש לאימקס שני תוספים עבור השלמות אוטומטיות: תוסף זה ותוסף בשם auto complete. אני משתמש ב company mode אבל עדיין לא מספיק מכיר אימקס כדי להסביר למה, כנראה זה הראשון שמצאתי. הוא עובד די טוב ויש תמיכה בהרבה שפות באמצעות הרחבות. כך למשל הגדרתי תמיכה ב JavaScript עם מנוע tern, השלמות פייתון עם מנוע jedi ולרובי עם תוסף שנקרא robe.

7. ועוד שתי קטנות מגיטהאב

שני התוספים האחרונים לא מופצים במנהל החבילות הרשמי אבל הצליחו לעזור לי להתגבר על שני דברים מאוד מעצבנים באימקס: הראשון שכבר כתבתי עליו הוא החלפת התיקיה האוטומטית בכל עריכת קובץ והשני הוא התנהגות בעת חיפוש. וים משאיר את תוצאות החיפוש מסומנות גם אחרי שעזבתם את מצב החיפוש ואילו אימקס מעלים את ההדגשה. 

התוסף הבא מתקן את בעיית מעבר התיקיות:
https://github.com/gbarta/evil-noautochdir

והתוסף הבא מתקן את בעיית החיפושים:
https://github.com/juanjux/evil-search-highlight-persist