Emacs config utilizing prelude as a base
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

461 lines
17 KiB

  1. ;; cucumber.el -- Emacs mode for editing plain text user stories
  2. ;;
  3. ;; Copyright (C) 2008 — 2011 Michael Klishin and other contributors
  4. ;;
  5. ;; This program is free software; you can redistribute it and/or
  6. ;; modify it under the terms of the GNU General Public License
  7. ;; as published by the Free Software Foundation; either version 2
  8. ;; of the License, or (at your option) any later version.
  9. ;;
  10. ;; This program is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;;
  15. ;; You should have received a copy of the GNU General Public License
  16. ;; along with this program; if not, write to the Free Software
  17. ;; Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110.
  18. ;;
  19. ;; Copy files to ~/.emacs.d/elisp/feature-mode and add this to your
  20. ;; .emacs to load the mode
  21. ;; (add-to-list 'load-path "~/.emacs.d/elisp/feature-mode")
  22. ;; ;; optional configurations
  23. ;; ;; default language if .feature doesn't have "# language: fi"
  24. ;; ;(setq feature-default-language "fi")
  25. ;; ;; point to cucumber languages.yml or gherkin i18n.yml to use
  26. ;; ;; exactly the same localization your cucumber uses
  27. ;; ;(setq feature-default-i18n-file "/path/to/gherkin/gem/i18n.yml")
  28. ;; ;; and load it
  29. ;; (require 'feature-mode)
  30. ;; (add-to-list 'auto-mode-alist '("\.feature$" . feature-mode))
  31. ;;
  32. ;; Language used in feature file is automatically detected from
  33. ;; "language: [2-letter ISO-code]" tag in feature file. You can
  34. ;; choose the language feature-mode should use in case autodetection
  35. ;; fails. Just add
  36. ;; (setq feature-default-language "en")
  37. ;; to your .emacs
  38. ;;
  39. ;; Translations are loaded from ~/.emacs.d/elisp/feature-mode/i18n.yml
  40. ;; by default. You can configure feature-mode to load translations
  41. ;; directly from cucumber languages.yml or gherkin i18n.yml. Just add
  42. ;; (setq feature-default-i18n-file
  43. ;; "/usr/lib/ruby/gems/1.8/gems/cucumber-0.4.4/lib/cucumber/languages.yml")
  44. ;; to your .emacs before
  45. ;; (require 'feature-mode)
  46. ;;
  47. ;;
  48. ;; In order to get goto-step-definition to work, you must install the
  49. ;; ruby_parser gem (version 2.0.x). For example:
  50. ;;
  51. ;; gem install ruby_parser --version=2.0.5
  52. ;;
  53. ;; (be sure and use the ruby-interpreter that emacs will use based on
  54. ;; `exec-path')
  55. ;;
  56. ;;
  57. ;; Key Bindings
  58. ;; ------------
  59. ;;
  60. ;; \C-c ,v
  61. ;; : Verify all scenarios in the current buffer file.
  62. ;;
  63. ;; \C-c ,s
  64. ;; : Verify the scenario under the point in the current buffer.
  65. ;;
  66. ;; \C-c ,f
  67. ;; : Verify all features in project. (Available in feature and
  68. ;; ruby files)
  69. ;;
  70. ;; \C-c ,r
  71. ;; : Repeat the last verification process.
  72. ;;
  73. ;; \C-c ,g
  74. ;; : Go to step-definition under point
  75. (eval-when-compile (require 'cl))
  76. (require 'thingatpt)
  77. ;;
  78. ;; Keywords and font locking
  79. ;;
  80. (when (featurep 'font-lock)
  81. (or (boundp 'font-lock-variable-name-face)
  82. (setq font-lock-variable-name-face font-lock-type-face)))
  83. (defun load-gherkin-i10n (filename)
  84. "Read and parse Gherkin l10n from given file."
  85. (interactive "Load l10n file: ")
  86. (with-temp-buffer
  87. (insert-file-contents filename)
  88. (parse-gherkin-l10n)))
  89. (defun parse-gherkin-l10n ()
  90. (let (languages-alist)
  91. (save-excursion
  92. (goto-char (point-min))
  93. (while (not (eobp))
  94. (if (try-find-next-language)
  95. (let ((lang-beg (+ (point) 1))
  96. (lang-end (progn (end-of-line) (- (point) 2)))
  97. (kwds-beg (+ (point) 1))
  98. (kwds-end (progn (try-find-next-language) (point))))
  99. (add-to-list
  100. 'languages-alist
  101. (cons
  102. (filter-buffer-substring lang-beg lang-end)
  103. (parse-gherkin-l10n-translations kwds-beg kwds-end)))))))
  104. (nreverse languages-alist)))
  105. (defun try-find-next (regexp)
  106. (let (search-result)
  107. (setq search-result (search-forward-regexp regexp nil t))
  108. (if search-result
  109. (beginning-of-line)
  110. (goto-char (point-max)))
  111. search-result))
  112. (defun try-find-next-language ()
  113. (try-find-next "^\"[^\"]+\":"))
  114. (defun try-find-next-translation ()
  115. (try-find-next "^ \\([^ :]+\\): +\"?\\*?|?\\([^\"\n]+\\)\"?"))
  116. (defun parse-gherkin-l10n-translations (beg end)
  117. (let (translations-alist)
  118. (save-excursion
  119. (save-restriction
  120. (narrow-to-region beg end)
  121. (goto-char (point-min))
  122. (while (not (eobp))
  123. (if (try-find-next-translation)
  124. (let ((kwname (match-string-no-properties 1))
  125. (kw (match-string-no-properties 2)))
  126. (add-to-list
  127. 'translations-alist
  128. (cons
  129. (intern kwname)
  130. (if (or (equal kwname "name")
  131. (equal kwname "native"))
  132. kw
  133. (build-keyword-matcher kw))))))
  134. (end-of-line))))
  135. (nreverse translations-alist)))
  136. (defun build-keyword-matcher (keyword)
  137. (concat "^[ \t]*\\(" (replace-regexp-in-string "|" "\\\\|" keyword) "\\):?"))
  138. (defvar feature-default-language "en")
  139. (defvar feature-default-i18n-file "~/.emacs.d/elisp/feature-mode/i18n.yml")
  140. (defconst feature-keywords-per-language
  141. (if (file-readable-p feature-default-i18n-file)
  142. (load-gherkin-i10n feature-default-i18n-file)
  143. '(("en" . ((feature . "^ *Feature:")
  144. (background . "^ *Background:")
  145. (scenario . "^ *Scenario:")
  146. (scenario_outline .
  147. "^ *Scenario Outline:")
  148. (given . "^ *Given")
  149. (when . "^ *When")
  150. (then . "^ *Then")
  151. (but . "^ *But")
  152. (and . "^ *And")
  153. (examples . "^ *\\(Examples\\|Scenarios\\):?"))))))
  154. (defconst feature-font-lock-keywords
  155. '((feature (0 font-lock-keyword-face)
  156. (".*" nil nil (0 font-lock-type-face t)))
  157. (background . (0 font-lock-keyword-face))
  158. (scenario (0 font-lock-keyword-face)
  159. (".*" nil nil (0 font-lock-function-name-face t)))
  160. (scenario_outline
  161. (0 font-lock-keyword-face)
  162. (".*" nil nil (0 font-lock-function-name-face t)))
  163. (given . font-lock-keyword-face)
  164. (when . font-lock-keyword-face)
  165. (then . font-lock-keyword-face)
  166. (but . font-lock-keyword-face)
  167. (and . font-lock-keyword-face)
  168. (examples . font-lock-keyword-face)
  169. ("^ *@.*" . font-lock-preprocessor-face)
  170. ("^ *#.*" 0 font-lock-comment-face t)))
  171. ;;
  172. ;; Keymap
  173. ;;
  174. (defvar feature-mode-map nil "Keymap used in feature mode")
  175. (if feature-mode-map
  176. nil
  177. (setq feature-mode-map (make-sparse-keymap))
  178. (define-key feature-mode-map "\C-m" 'newline)
  179. (define-key feature-mode-map (kbd "C-c ,s") 'feature-verify-scenario-at-pos)
  180. (define-key feature-mode-map (kbd "C-c ,v") 'feature-verify-all-scenarios-in-buffer)
  181. (define-key feature-mode-map (kbd "C-c ,f") 'feature-verify-all-scenarios-in-project)
  182. (define-key feature-mode-map (kbd "C-c ,g") 'feature-goto-step-definition))
  183. ;; Add relevant feature keybindings to ruby modes
  184. (add-hook 'ruby-mode-hook
  185. (lambda ()
  186. (local-set-key (kbd "C-c ,f") 'feature-verify-all-scenarios-in-project)))
  187. ;;
  188. ;; Syntax table
  189. ;;
  190. (defvar feature-mode-syntax-table nil
  191. "Syntax table in use in ruby-mode buffers.")
  192. (unless feature-mode-syntax-table
  193. (setq feature-mode-syntax-table (make-syntax-table)))
  194. ;; Constants
  195. (defconst feature-blank-line-re "^[ \t]*$"
  196. "Regexp matching a line containing only whitespace.")
  197. (defun feature-feature-re (language)
  198. (cdr (assoc 'feature (cdr (assoc language feature-keywords-per-language)))))
  199. (defun feature-scenario-re (language)
  200. (cdr (assoc 'scenario (cdr (assoc language feature-keywords-per-language)))))
  201. (defun feature-background-re (language)
  202. (cdr (assoc 'background (cdr (assoc language feature-keywords-per-language)))))
  203. ;;
  204. ;; Variables
  205. ;;
  206. (defvar feature-mode-hook nil
  207. "Hook run when entering `feature-mode'.")
  208. (defcustom feature-indent-level 2
  209. "Indentation of feature statements"
  210. :type 'integer :group 'feature)
  211. (defcustom feature-indent-offset 2
  212. "*Amount of offset per level of indentation."
  213. :type 'integer :group 'feature)
  214. (defun feature-compute-indentation ()
  215. "Calculate the maximum sensible indentation for the current line."
  216. (save-excursion
  217. (beginning-of-line)
  218. (if (bobp) 10
  219. (forward-line -1)
  220. (while (and (looking-at feature-blank-line-re)
  221. (> (point) (point-min)))
  222. (forward-line -1))
  223. (+ (current-indentation)
  224. (if (or (looking-at (feature-feature-re (feature-detect-language)))
  225. (looking-at (feature-scenario-re (feature-detect-language)))
  226. (looking-at (feature-background-re (feature-detect-language))))
  227. feature-indent-offset 0)))))
  228. (defun feature-indent-line ()
  229. "Indent the current line.
  230. The first time this command is used, the line will be indented to the
  231. maximum sensible indentation. Each immediately subsequent usage will
  232. back-dent the line by `feature-indent-offset' spaces. On reaching column
  233. 0, it will cycle back to the maximum sensible indentation."
  234. (interactive "*")
  235. (let ((ci (current-indentation))
  236. (cc (current-column))
  237. (need (feature-compute-indentation)))
  238. (save-excursion
  239. (beginning-of-line)
  240. (delete-horizontal-space)
  241. (if (and (equal last-command this-command) (/= ci 0))
  242. (indent-to (* (/ (- ci 1) feature-indent-offset) feature-indent-offset))
  243. (indent-to need)))
  244. (if (< (current-column) (current-indentation))
  245. (forward-to-indentation 0))))
  246. (defun feature-font-lock-keywords-for (language)
  247. (let ((result-keywords . ()))
  248. (dolist (pair feature-font-lock-keywords)
  249. (let* ((keyword (car pair))
  250. (font-locking (cdr pair))
  251. (language-keyword (cdr (assoc keyword
  252. (cdr (assoc
  253. language
  254. feature-keywords-per-language))))))
  255. (push (cons (or language-keyword keyword) font-locking) result-keywords)))
  256. result-keywords))
  257. (defun feature-detect-language ()
  258. (save-excursion
  259. (goto-char (point-min))
  260. (if (re-search-forward "language: \\([[:alpha:]-]+\\)"
  261. (line-end-position)
  262. t)
  263. (match-string 1)
  264. feature-default-language)))
  265. (defun feature-mode-variables ()
  266. (set-syntax-table feature-mode-syntax-table)
  267. (setq require-final-newline t)
  268. (setq comment-start "# ")
  269. (setq comment-start-skip "#+ *")
  270. (setq comment-end "")
  271. (setq parse-sexp-ignore-comments t)
  272. (set (make-local-variable 'indent-tabs-mode) 'nil)
  273. (set (make-local-variable 'indent-line-function) 'feature-indent-line)
  274. (set (make-local-variable 'font-lock-defaults)
  275. (list (feature-font-lock-keywords-for (feature-detect-language)) nil nil))
  276. (set (make-local-variable 'font-lock-keywords)
  277. (feature-font-lock-keywords-for (feature-detect-language))))
  278. (defun feature-minor-modes ()
  279. "Enable all minor modes for feature mode."
  280. (turn-on-orgtbl))
  281. ;;
  282. ;; Mode function
  283. ;;
  284. ;;;###autoload
  285. (defun feature-mode()
  286. "Major mode for editing plain text stories"
  287. (interactive)
  288. (kill-all-local-variables)
  289. (use-local-map feature-mode-map)
  290. (setq mode-name "Feature")
  291. (setq major-mode 'feature-mode)
  292. (feature-mode-variables)
  293. (feature-minor-modes)
  294. (run-mode-hooks 'feature-mode-hook))
  295. (add-to-list 'auto-mode-alist '("\\.feature\\'" . feature-mode))
  296. ;;
  297. ;; Snippets
  298. ;;
  299. (defvar feature-snippet-directory (concat (file-name-directory load-file-name) "snippets")
  300. "Path to the feature-mode snippets.
  301. If the yasnippet library is loaded, snippets in this directory
  302. are loaded on startup. If nil, don't load snippets.")
  303. (defvar feature-support-directory (concat (file-name-directory load-file-name) "support")
  304. "Path to support folder
  305. The support folder contains a ruby script that takes a step as an
  306. argument, and outputs a list of all matching step definitions")
  307. (declare-function yas/load-directory "yasnippet" t)
  308. (when (and (featurep 'yasnippet)
  309. feature-snippet-directory
  310. (file-exists-p feature-snippet-directory))
  311. (yas/load-directory feature-snippet-directory))
  312. ;;
  313. ;; Verifying features
  314. ;;
  315. (defun feature-scenario-name-re (language)
  316. (concat (feature-scenario-re (feature-detect-language)) "[[:space:]]+\\(.*\\)$"))
  317. (defun feature-scenario-name-at-pos (&optional pos)
  318. "Returns the name of the scenario at the specified position. if pos is not specified the current buffer location will be used."
  319. (interactive)
  320. (let ((start (or pos (point))))
  321. (save-excursion
  322. (end-of-line)
  323. (unless (re-search-backward (feature-scenario-name-re (feature-detect-language)) nil t)
  324. (error "Unable to find an scenario"))
  325. (match-string-no-properties 1))))
  326. (defun feature-verify-scenario-at-pos (&optional pos)
  327. "Run the scenario defined at pos. If post is not specified the current buffer location will be used."
  328. (interactive)
  329. (feature-run-cucumber
  330. (list "-n" (concat "'" (feature-escape-scenario-name (feature-scenario-name-at-pos)) "'"))
  331. :feature-file (buffer-file-name)))
  332. (defun feature-verify-all-scenarios-in-buffer ()
  333. "Run all the scenarios defined in current buffer."
  334. (interactive)
  335. (feature-run-cucumber '() :feature-file (buffer-file-name)))
  336. (defun feature-verify-all-scenarios-in-project ()
  337. "Run all the scenarios defined in current project."
  338. (interactive)
  339. (feature-run-cucumber '()))
  340. (defun feature-register-verify-redo (redoer)
  341. "Register a bit of code that will repeat a verification process"
  342. (let ((redoer-cmd (eval (list 'lambda ()
  343. '(interactive)
  344. (list 'let (list (list `default-directory
  345. default-directory))
  346. redoer)))))
  347. (global-set-key (kbd "C-c ,r") redoer-cmd)))
  348. (defun feature-run-cucumber (cuke-opts &optional &key feature-file)
  349. "Runs cucumber with the specified options"
  350. (feature-register-verify-redo (list 'feature-run-cucumber
  351. (list 'quote cuke-opts)
  352. :feature-file feature-file))
  353. ;; redoer is registered
  354. (let ((opts-str (mapconcat 'identity cuke-opts " "))
  355. (feature-arg (if feature-file
  356. (concat " FEATURE='" feature-file "'")
  357. "")))
  358. (ansi-color-for-comint-mode-on)
  359. (let ((default-directory (feature-project-root)))
  360. (compile (concat "rake cucumber CUCUMBER_OPTS=\"" opts-str "\"" feature-arg) t)))
  361. (end-of-buffer-other-window 0))
  362. (defun feature-escape-scenario-name (scenario-name)
  363. "Escapes all the characaters in a scenario name that mess up using in the -n options"
  364. (replace-regexp-in-string "\\(\"\\)" "\\\\\\\\\\\\\\1" (replace-regexp-in-string "\\([()\']\\|\\[\\|\\]\\)" "\\\\\\1" scenario-name)))
  365. (defun feature-root-directory-p (a-directory)
  366. "Tests if a-directory is the root of the directory tree (i.e. is it '/' on unix)."
  367. (equal a-directory (file-name-directory (directory-file-name a-directory))))
  368. (defun feature-project-root (&optional directory)
  369. "Finds the root directory of the project by walking the directory tree until it finds Rakefile (presumably, application root)"
  370. (let ((directory (file-name-as-directory (or directory default-directory))))
  371. (if (feature-root-directory-p directory) (error "No rakefle found"))
  372. (if (file-exists-p (concat directory "Rakefile"))
  373. directory
  374. (feature-project-root (file-name-directory (directory-file-name directory))))))
  375. (defun feature-goto-step-definition ()
  376. "Goto the step-definition under (point). Requires ruby"
  377. (interactive)
  378. (let* ((root (feature-project-root))
  379. (input (thing-at-point 'line))
  380. (_ (set-text-properties 0 (length input) nil input))
  381. (result (shell-command-to-string (format "cd %S && ruby %S/go_to_step.rb %S"
  382. root
  383. feature-support-directory
  384. input)))
  385. (file-and-line (car (split-string result "\n")))
  386. (matched? (string-match "^\\(.+\\):\\([0-9]+\\)$" file-and-line)))
  387. (if matched?
  388. (let ((file (format "%s/%s" root (match-string 1 file-and-line)))
  389. (line-no (string-to-number (match-string 2 file-and-line))))
  390. (find-file file)
  391. (goto-char (point-min))
  392. (forward-line (1- line-no)))
  393. (if (equal "" result)
  394. (message "No matching steps found for:\n%s" input)
  395. (message "An error occurred:\n%s" result)))))
  396. (provide 'cucumber-mode)
  397. (provide 'feature-mode)