Klimi's new dotfiles with stow.
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.

766 lines
29 KiB

4 years ago
  1. ;;; feature-mode.el --- Major mode for editing Gherkin (i.e. Cucumber) user stories
  2. ;;; Version: 0.4
  3. ;;; Author: Michael Klishin
  4. ;;; URL: https://github.com/michaelklishin/cucumber.el
  5. ;;; Uploader: Kao Félix
  6. ;; Copyright (C) 2008 — 2012 Michael Klishin and other contributors
  7. ;;
  8. ;; This program is free software; you can redistribute it and/or
  9. ;; modify it under the terms of the GNU General Public License
  10. ;; as published by the Free Software Foundation; either version 2
  11. ;; of the License, or (at your option) any later version.
  12. ;;
  13. ;; This program is distributed in the hope that it will be useful,
  14. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;; GNU General Public License for more details.
  17. ;;
  18. ;; You should have received a copy of the GNU General Public License
  19. ;; along with this program; if not, write to the Free Software
  20. ;; Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110.
  21. ;;
  22. ;; Copy files to ~/.emacs.d/elisp/feature-mode and add this to your
  23. ;; .emacs to load the mode
  24. ;; (add-to-list 'load-path "~/.emacs.d/elisp/feature-mode")
  25. ;; ;; optional configurations
  26. ;; ;; default language if .feature doesn't have "# language: fi"
  27. ;; ;(setq feature-default-language "fi")
  28. ;; ;; point to cucumber languages.yml or gherkin i18n.yml to use
  29. ;; ;; exactly the same localization your cucumber uses
  30. ;; ;(setq feature-default-i18n-file "/path/to/gherkin/gem/i18n.yml")
  31. ;; ;; and load it
  32. ;; (require 'feature-mode)
  33. ;; (add-to-list 'auto-mode-alist '("\.feature$" . feature-mode))
  34. ;;
  35. ;; If using RVM, set `feature-use-rvm' to `t' to enable RVM
  36. ;; support. This requires `rvm.el'.
  37. ;;
  38. ;; If using Chruby, set `feature-use-chruby' to `t' to enable
  39. ;; Chruby support. This requires `chruby.el'
  40. ;;
  41. ;; Language used in feature file is automatically detected from
  42. ;; "language: [2-letter ISO-code]" tag in feature file. You can
  43. ;; choose the language feature-mode should use in case autodetection
  44. ;; fails. Just add
  45. ;; (setq feature-default-language "en")
  46. ;; to your .emacs
  47. ;;
  48. ;; Translations are loaded from ~/.emacs.d/elisp/feature-mode/i18n.yml
  49. ;; by default. You can configure feature-mode to load translations
  50. ;; directly from cucumber languages.yml or gherkin i18n.yml. Just add
  51. ;; (setq feature-default-i18n-file
  52. ;; "/usr/lib/ruby/gems/1.8/gems/cucumber-0.4.4/lib/cucumber/languages.yml")
  53. ;; to your .emacs before
  54. ;; (require 'feature-mode)
  55. ;;
  56. ;;
  57. ;; In order to get goto-step-definition to work, you must install the
  58. ;; ruby_parser gem (version 2.0.x). For example:
  59. ;;
  60. ;; gem install ruby_parser --version=2.0.5
  61. ;;
  62. ;; (be sure and use the ruby-interpreter that emacs will use based on
  63. ;; `exec-path')
  64. ;;
  65. ;;
  66. ;; Key Bindings
  67. ;; ------------
  68. ;;
  69. ;; \C-c ,v
  70. ;; : Verify all scenarios in the current buffer file.
  71. ;;
  72. ;; \C-c ,s
  73. ;; : Verify the scenario under the point in the current buffer.
  74. ;;
  75. ;; \C-c ,f
  76. ;; : Verify all features in project. (Available in feature and
  77. ;; ruby files)
  78. ;;
  79. ;; \C-c ,r
  80. ;; : Repeat the last verification process.
  81. ;;
  82. ;; \C-c ,g
  83. ;; : Go to step-definition under point
  84. (eval-when-compile (require 'cl))
  85. (require 'thingatpt)
  86. (require 'etags)
  87. (defcustom feature-cucumber-command "cucumber {options} \"{feature}\""
  88. "command used to run cucumber when there is no Rakefile"
  89. :group 'feature-mode
  90. :type 'string)
  91. (defcustom feature-rake-command "rake cucumber CUCUMBER_OPTS=\"{options}\" FEATURE=\"{feature}\""
  92. "command used to run cucumber when there is a Rakefile"
  93. :group 'feature-mode
  94. :type 'string)
  95. (defcustom feature-enable-back-denting t
  96. "when enabled, subsequent pressing the tab key back-dents the current line by `feature-indent-offset' spaces"
  97. :type 'boolean
  98. :group 'feature-mode)
  99. (defcustom feature-use-rvm nil
  100. "t when RVM is in use. (Requires rvm.el)"
  101. :type 'boolean
  102. :group 'feature-mode)
  103. (defcustom feature-use-chruby nil
  104. "t when Chruby is in use. (Requires chruby.el)"
  105. :type 'boolean
  106. :group 'feature-mode)
  107. (defcustom feature-root-marker-file-name "features"
  108. "file to look for to find the project root."
  109. :group 'feature-mode
  110. :type 'string)
  111. (defcustom feature-align-steps-after-first-word nil
  112. "when set to t, make step lines align on the space after the first word"
  113. :type 'boolean
  114. :group 'feature-mode)
  115. (defcustom feature-step-search-path "features/**/*steps.rb"
  116. "Path to project step definitions"
  117. :type 'string
  118. :group 'feature-mode)
  119. (defcustom feature-step-search-gems-path "gems/ruby/*/gems/*/**/*steps.rb"
  120. "Path to find step definitions in installed gems"
  121. :type 'string
  122. :group 'feature-mode)
  123. (defcustom feature-ruby-command "ruby"
  124. "Command to run ruby"
  125. :type 'string
  126. :group 'feature-mode)
  127. ;; Docker related
  128. (defcustom feature-use-docker-compose t
  129. "Use docker-compose when docker-compose.yml exists in project."
  130. :type 'boolean
  131. :group 'feature-mode)
  132. (defcustom feature-docker-compose-command "docker-compose"
  133. "Command to run docker-compose."
  134. :type 'string
  135. :group 'feature-mode)
  136. (defcustom feature-docker-compose-container "app"
  137. "The container to run cucumber in."
  138. :type 'string
  139. :group 'feature-mode)
  140. ;;
  141. ;; Keywords and font locking
  142. ;;
  143. (when (featurep 'font-lock)
  144. (or (boundp 'font-lock-variable-name-face)
  145. (setq font-lock-variable-name-face font-lock-type-face)))
  146. (defun load-gherkin-i10n (filename)
  147. "Read and parse Gherkin l10n from given file."
  148. (interactive "Load l10n file: ")
  149. (with-temp-buffer
  150. (insert-file-contents filename)
  151. (parse-gherkin-l10n)))
  152. (defun parse-gherkin-l10n ()
  153. (let (languages-alist)
  154. (save-excursion
  155. (goto-char (point-min))
  156. (while (not (eobp))
  157. (if (try-find-next-language)
  158. (let ((lang-beg (+ (point) 1))
  159. (lang-end (progn (end-of-line) (- (point) 2)))
  160. (kwds-beg (+ (point) 1))
  161. (kwds-end (progn (try-find-next-language) (point))))
  162. (add-to-list
  163. 'languages-alist
  164. (cons
  165. (filter-buffer-substring lang-beg lang-end)
  166. (parse-gherkin-l10n-translations kwds-beg kwds-end)))))))
  167. (nreverse languages-alist)))
  168. (defun try-find-next (regexp)
  169. (let (search-result)
  170. (setq search-result (search-forward-regexp regexp nil t))
  171. (if search-result
  172. (beginning-of-line)
  173. (goto-char (point-max)))
  174. search-result))
  175. (defun try-find-next-language ()
  176. (try-find-next "^\"[^\"]+\":"))
  177. (defun try-find-next-translation ()
  178. (try-find-next "^ \\([^ :]+\\): +\"?\\*?|?\\([^\"\n]+\\)\"?"))
  179. (defun parse-gherkin-l10n-translations (beg end)
  180. (let (translations-alist)
  181. (save-excursion
  182. (save-restriction
  183. (narrow-to-region beg end)
  184. (goto-char (point-min))
  185. (while (not (eobp))
  186. (if (try-find-next-translation)
  187. (let ((kwname (match-string-no-properties 1))
  188. (kw (match-string-no-properties 2)))
  189. (add-to-list
  190. 'translations-alist
  191. (cons
  192. (intern kwname)
  193. (if (or (equal kwname "name")
  194. (equal kwname "native"))
  195. kw
  196. (build-keyword-matcher kw))))))
  197. (end-of-line))))
  198. (nreverse translations-alist)))
  199. (defun build-keyword-matcher (keyword)
  200. (concat "^[ \t]*\\(" (replace-regexp-in-string "|" "\\\\|" keyword) "\\):?"))
  201. (defvar feature-default-language "en")
  202. (defvar feature-default-directory "features")
  203. (defvar feature-default-i18n-file (expand-file-name (concat (file-name-directory load-file-name) "/i18n.yml")))
  204. (defconst feature-keywords-per-language
  205. (if (file-readable-p feature-default-i18n-file)
  206. (load-gherkin-i10n feature-default-i18n-file)
  207. '(("en" . ((feature . "^ *\\(Feature\\):")
  208. (background . "^ *\\(Background\\):")
  209. (scenario . "^ *\\(Scenario\\):")
  210. (scenario_outline .
  211. "^ *\\(Scenario Outline\\):")
  212. (given . "^ *\\(Given\\) ")
  213. (when . "^ *\\(When\\) ")
  214. (then . "^ *\\(Then\\) ")
  215. (but . "^ *\\(But\\) ")
  216. (and . "^ *\\(And\\) ")
  217. (examples . "^ *\\(Examples\\|Scenarios\\):"))))))
  218. (defconst feature-font-lock-keywords
  219. '((feature (0 font-lock-keyword-face)
  220. (".*" nil nil (0 font-lock-type-face t)))
  221. (background . (0 font-lock-keyword-face))
  222. (scenario (0 font-lock-keyword-face)
  223. (".*" nil nil (0 font-lock-function-name-face nil)))
  224. (scenario_outline
  225. (0 font-lock-keyword-face)
  226. (".*" nil nil (0 font-lock-function-name-face t)))
  227. (given . font-lock-keyword-face)
  228. (when . font-lock-keyword-face)
  229. (then . font-lock-keyword-face)
  230. (but . font-lock-keyword-face)
  231. (and . font-lock-keyword-face)
  232. (examples . font-lock-keyword-face)
  233. ("<[^>]*>" . font-lock-variable-name-face)
  234. ("^ *@.*" . font-lock-preprocessor-face)
  235. ("^ *#.*" 0 font-lock-comment-face t)))
  236. ;;
  237. ;; Keymap
  238. ;;
  239. (defvar feature-mode-map nil "Keymap used in feature mode")
  240. (if feature-mode-map
  241. nil
  242. (setq feature-mode-map (make-sparse-keymap))
  243. (define-key feature-mode-map "\C-m" 'newline)
  244. (define-key feature-mode-map (kbd "C-c ,s") 'feature-verify-scenario-at-pos)
  245. (define-key feature-mode-map (kbd "C-c ,v") 'feature-verify-all-scenarios-in-buffer)
  246. (define-key feature-mode-map (kbd "C-c ,f") 'feature-verify-all-scenarios-in-project)
  247. (define-key feature-mode-map (kbd "C-c ,g") 'feature-goto-step-definition)
  248. (define-key feature-mode-map (kbd "M-.") 'feature-goto-step-definition))
  249. ;; Add relevant feature keybindings to ruby modes
  250. (add-hook 'ruby-mode-hook
  251. (lambda ()
  252. (local-set-key (kbd "C-c ,f") 'feature-verify-all-scenarios-in-project)))
  253. ;;
  254. ;; Syntax table
  255. ;;
  256. (defvar feature-mode-syntax-table nil
  257. "Syntax table in use in ruby-mode buffers.")
  258. (unless feature-mode-syntax-table
  259. (setq feature-mode-syntax-table (make-syntax-table)))
  260. ;; Constants
  261. (defconst feature-blank-line-re "^[ \t]*\\(?:#.*\\)?$"
  262. "Regexp matching a line containing only whitespace.")
  263. (defconst feature-example-line-re "^[ \t]*|"
  264. "Regexp matching a line containing scenario example.")
  265. (defconst feature-tag-line-re "^[ \t]*@"
  266. "Regexp matching a tag/annotation")
  267. (defconst feature-pystring-re "^[ \t]*\"\"\"$"
  268. "Regexp matching a pystring")
  269. (defun feature-feature-re (language)
  270. (cdr (assoc 'feature (cdr (assoc language feature-keywords-per-language)))))
  271. (defun feature-scenario-re (language)
  272. (cdr (assoc 'scenario (cdr (assoc language feature-keywords-per-language)))))
  273. (defun feature-examples-re (language)
  274. (cdr (assoc 'examples (cdr (assoc language feature-keywords-per-language)))))
  275. (defun feature-background-re (language)
  276. (cdr (assoc 'background (cdr (assoc language feature-keywords-per-language)))))
  277. (defun feature-given-re (language)
  278. (cdr (assoc 'given (cdr (assoc language feature-keywords-per-language)))))
  279. (defun feature-when-re (language)
  280. (cdr (assoc 'when (cdr (assoc language feature-keywords-per-language)))))
  281. (defun feature-then-re (language)
  282. (cdr (assoc 'then (cdr (assoc language feature-keywords-per-language)))))
  283. (defun feature-and-re (language)
  284. (cdr (assoc 'and (cdr (assoc language feature-keywords-per-language)))))
  285. (defun feature-but-re (language)
  286. (cdr (assoc 'but (cdr (assoc language feature-keywords-per-language)))))
  287. ;;
  288. ;; Variables
  289. ;;
  290. (defvar feature-mode-hook nil
  291. "Hook run when entering `feature-mode'.")
  292. (defcustom feature-indent-initial-offset 0
  293. "Indentation of the first file"
  294. :type 'integer :group 'feature-mode)
  295. (defcustom feature-indent-level 2
  296. "Indentation of feature statements"
  297. :type 'integer :group 'feature-mode)
  298. (defcustom feature-indent-offset 2
  299. "*Amount of offset per level of indentation."
  300. :type 'integer :group 'feature-mode)
  301. (defun given-when-then-wordlength (lang)
  302. (let* ((when-then-and-words '(given when then and but))
  303. (language-keywords (cdr (assoc lang feature-keywords-per-language)))
  304. (rexes (append (mapcar
  305. (lambda (kw) (cdr (assoc kw language-keywords)))
  306. when-then-and-words))))
  307. (beginning-of-line)
  308. ;; white-space means offset -1
  309. (if (or (bobp) (eobp))
  310. nil
  311. (if (looking-at feature-blank-line-re)
  312. 0
  313. (if (some (lambda (rex) (looking-at rex)) rexes)
  314. (length (match-string 1))
  315. nil)))))
  316. (defun compute-given-when-then-offset (lang)
  317. (if feature-align-steps-after-first-word
  318. (progn
  319. (setq current-word-length (given-when-then-wordlength lang))
  320. (cond
  321. ;; a non-given-when-then-line doesn't adjust the
  322. ;; offset
  323. ((null current-word-length) 0)
  324. ;; the same happens for empty lines
  325. ((= 0 current-word-length) 0)
  326. ;; we are on a proper line, figure out
  327. ;; the lengths of all lines preceding us
  328. (t (let ((search (lambda (direction lang)
  329. (forward-line direction)
  330. (setq search-word-length (given-when-then-wordlength lang))
  331. (cond
  332. ((null search-word-length) nil)
  333. (t (cons search-word-length (funcall search direction lang)))))))
  334. (setq previous-lengths (delq 0 (save-excursion
  335. (funcall search -1 lang))))
  336. (if (not (null previous-lengths))
  337. (- (car previous-lengths) current-word-length)
  338. 0)))))
  339. 0))
  340. (defun feature-search-for-regex-match (key)
  341. "Search for matching regexp on each line"
  342. (forward-line -1)
  343. (while (and (not (funcall key)) (> (point) (point-min)))
  344. (forward-line -1))
  345. )
  346. (defun feature-compute-indentation ()
  347. "Calculate the maximum sensible indentation for the current line."
  348. (save-excursion
  349. (beginning-of-line)
  350. (if (bobp) feature-indent-initial-offset
  351. (let* ((lang (feature-detect-language))
  352. (given-when-then-offset (compute-given-when-then-offset lang))
  353. (saved-indentation (current-indentation)))
  354. (cond
  355. ((looking-at (feature-feature-re lang))
  356. (progn
  357. (feature-search-for-regex-match (lambda () (looking-at (feature-feature-re lang))))
  358. (current-indentation)
  359. ))
  360. ((or (looking-at (feature-background-re lang))
  361. (looking-at (feature-scenario-re lang))
  362. (looking-at feature-tag-line-re))
  363. (progn
  364. (feature-search-for-regex-match
  365. (lambda () (or (looking-at (feature-feature-re lang))
  366. (looking-at feature-tag-line-re)
  367. (looking-at (feature-background-re lang))
  368. (looking-at (feature-scenario-re lang)))))
  369. (cond
  370. ((or (looking-at (feature-feature-re lang))
  371. (looking-at feature-tag-line-re)
  372. ) feature-indent-level)
  373. ((or (looking-at (feature-background-re lang))
  374. (looking-at (feature-scenario-re lang))
  375. ) (current-indentation))
  376. (t saved-indentation))
  377. ))
  378. ((looking-at (feature-examples-re lang))
  379. (progn
  380. (feature-search-for-regex-match
  381. (lambda () (or (looking-at (feature-background-re lang))
  382. (looking-at (feature-scenario-re lang)))))
  383. (if (or (looking-at (feature-background-re lang)) (looking-at (feature-scenario-re lang)))
  384. (+ (current-indentation) feature-indent-offset)
  385. saved-indentation)
  386. ))
  387. ((or (looking-at (feature-given-re lang))
  388. (looking-at (feature-when-re lang))
  389. (looking-at (feature-then-re lang))
  390. (looking-at (feature-and-re lang))
  391. (looking-at (feature-but-re lang)))
  392. (progn
  393. (feature-search-for-regex-match
  394. (lambda () (or (looking-at (feature-background-re lang))
  395. (looking-at (feature-scenario-re lang))
  396. (looking-at (feature-given-re lang))
  397. (looking-at (feature-when-re lang))
  398. (looking-at (feature-then-re lang))
  399. (looking-at (feature-and-re lang))
  400. (looking-at (feature-but-re lang)))))
  401. (cond
  402. ((or (looking-at (feature-background-re lang)) (looking-at (feature-scenario-re lang)))
  403. (+ (current-indentation) feature-indent-offset))
  404. ((or (looking-at (feature-given-re lang))
  405. (looking-at (feature-when-re lang))
  406. (looking-at (feature-then-re lang))
  407. (looking-at (feature-and-re lang))
  408. (looking-at (feature-but-re lang)))
  409. (current-indentation))
  410. (t saved-indentation))
  411. ))
  412. ((or (looking-at feature-example-line-re) (looking-at feature-pystring-re))
  413. (progn
  414. (feature-search-for-regex-match
  415. (lambda () (or (looking-at (feature-examples-re lang))
  416. (looking-at (feature-given-re lang))
  417. (looking-at (feature-when-re lang))
  418. (looking-at (feature-then-re lang))
  419. (looking-at (feature-and-re lang))
  420. (looking-at (feature-but-re lang))
  421. (looking-at feature-example-line-re))))
  422. (cond
  423. ((or (looking-at (feature-examples-re lang))
  424. (looking-at (feature-given-re lang))
  425. (looking-at (feature-when-re lang))
  426. (looking-at (feature-then-re lang))
  427. (looking-at (feature-and-re lang))
  428. (looking-at (feature-but-re lang)))
  429. (+ (current-indentation) feature-indent-offset))
  430. ((or (looking-at feature-example-line-re)
  431. (looking-at feature-pystring-re))
  432. (current-indentation))
  433. (t saved-indentation))
  434. ))
  435. (t
  436. (progn
  437. (feature-search-for-regex-match (lambda () (not (looking-at feature-blank-line-re))))
  438. (+ (current-indentation)
  439. given-when-then-offset
  440. (if (or (looking-at (feature-feature-re lang))
  441. (looking-at (feature-scenario-re lang))
  442. (looking-at (feature-background-re lang)))
  443. feature-indent-offset 0))
  444. ))
  445. )))))
  446. (defun feature-indent-line ()
  447. "Indent the current line.
  448. The first time this command is used, the line will be indented to the
  449. maximum sensible indentation. Each immediately subsequent usage will
  450. back-dent the line by `feature-indent-offset' spaces. On reaching column
  451. 0, it will cycle back to the maximum sensible indentation."
  452. (interactive "*")
  453. (let ((ci (current-indentation))
  454. (cc (current-column))
  455. (need (feature-compute-indentation)))
  456. (save-excursion
  457. (beginning-of-line)
  458. (delete-horizontal-space)
  459. (if (and (equal last-command this-command) (/= ci 0) feature-enable-back-denting (called-interactively-p 'any))
  460. (indent-to (* (/ (- ci 1) feature-indent-offset) feature-indent-offset))
  461. (indent-to need)))
  462. (if (< (current-column) (current-indentation))
  463. (forward-to-indentation 0))))
  464. (defadvice orgtbl-tab (before feature-indent-table-advice (&optional arg))
  465. "Table org mode ignores our indentation, lets force it."
  466. (feature-indent-line))
  467. (ad-activate 'orgtbl-tab)
  468. (defun feature-font-lock-keywords-for (language)
  469. (let ((result-keywords . ()))
  470. (dolist (pair feature-font-lock-keywords)
  471. (let* ((keyword (car pair))
  472. (font-locking (cdr pair))
  473. (language-keyword (cdr (assoc keyword
  474. (cdr (assoc
  475. language
  476. feature-keywords-per-language))))))
  477. (push (cons (or language-keyword keyword) font-locking) result-keywords)))
  478. result-keywords))
  479. (defun feature-detect-language ()
  480. (save-excursion
  481. (goto-char (point-min))
  482. (if (re-search-forward "language: \\([[:alpha:]-]+\\)"
  483. (line-end-position)
  484. t)
  485. (match-string 1)
  486. feature-default-language)))
  487. (defun feature-mode-variables ()
  488. (set-syntax-table feature-mode-syntax-table)
  489. (when mode-require-final-newline
  490. (setq require-final-newline t))
  491. (setq comment-start "# ")
  492. (setq comment-start-skip "#+ *")
  493. (setq comment-end "")
  494. (setq parse-sexp-ignore-comments t)
  495. (set (make-local-variable 'indent-tabs-mode) 'nil)
  496. (set (make-local-variable 'indent-line-function) 'feature-indent-line)
  497. (set (make-local-variable 'font-lock-defaults)
  498. (list (feature-font-lock-keywords-for (feature-detect-language)) nil nil))
  499. (set (make-local-variable 'font-lock-keywords)
  500. (feature-font-lock-keywords-for (feature-detect-language)))
  501. (set (make-local-variable 'imenu-generic-expression)
  502. `(("Scenario:" ,(feature-scenario-name-re (feature-detect-language)) 3)
  503. ("Background:" ,(feature-background-re (feature-detect-language)) 1))))
  504. (defun feature-minor-modes ()
  505. "Enable/disable all minor modes for feature mode."
  506. (turn-on-orgtbl)
  507. (set (make-local-variable 'electric-indent-functions)
  508. (list (lambda (arg) 'no-indent))))
  509. ;;
  510. ;; Mode function
  511. ;;
  512. ;;;###autoload
  513. (defun feature-mode()
  514. "Major mode for editing plain text stories"
  515. (interactive)
  516. (kill-all-local-variables)
  517. (use-local-map feature-mode-map)
  518. (setq mode-name "Feature")
  519. (setq major-mode 'feature-mode)
  520. (feature-mode-variables)
  521. (feature-minor-modes)
  522. (run-mode-hooks 'feature-mode-hook))
  523. ;;;###autoload
  524. (add-to-list 'auto-mode-alist '("\\.feature\\'" . feature-mode))
  525. ;;
  526. ;; Snippets
  527. ;;
  528. (defvar feature-snippet-directory (concat (file-name-directory load-file-name) "snippets")
  529. "Path to the feature-mode snippets.
  530. If the yasnippet library is loaded, snippets in this directory
  531. are loaded on startup. If nil, don't load snippets.")
  532. (defvar feature-support-directory (concat (file-name-directory load-file-name) "support")
  533. "Path to support folder
  534. The support folder contains a ruby script that takes a step as an
  535. argument, and outputs a list of all matching step definitions")
  536. (declare-function yas/load-directory "yasnippet" t)
  537. (when (and (featurep 'yasnippet)
  538. feature-snippet-directory
  539. (file-exists-p feature-snippet-directory))
  540. (yas/load-directory feature-snippet-directory))
  541. ;;
  542. ;; Verifying features
  543. ;;
  544. (defun feature-scenario-name-re (language)
  545. (concat (feature-scenario-re (feature-detect-language)) "\\( Outline:?\\)?[[:space:]]+\\(.*\\)$"))
  546. (defun feature-verify-scenario-at-pos (&optional pos)
  547. "Run the scenario defined at pos. If post is not specified the current buffer location will be used."
  548. (interactive)
  549. (feature-run-cucumber
  550. (list "-l" (number-to-string (line-number-at-pos)))
  551. :feature-file (buffer-file-name)))
  552. (defun feature-verify-all-scenarios-in-buffer ()
  553. "Run all the scenarios defined in current buffer."
  554. (interactive)
  555. (feature-run-cucumber '() :feature-file (buffer-file-name)))
  556. (defun feature-verify-all-scenarios-in-project ()
  557. "Run all the scenarios defined in current project."
  558. (interactive)
  559. (feature-run-cucumber '()))
  560. (defun feature-register-verify-redo (redoer)
  561. "Register a bit of code that will repeat a verification process"
  562. (let ((redoer-cmd (eval (list 'lambda ()
  563. '(interactive)
  564. (list 'let (list (list `default-directory
  565. default-directory))
  566. redoer)))))
  567. (global-set-key (kbd "C-c ,r") redoer-cmd)))
  568. (defun project-file-exists (filename)
  569. "Determines if the project has a file"
  570. (file-exists-p (concat (feature-project-root) filename)))
  571. (defun can-run-bundle ()
  572. "Determines if bundler is installed and a Gemfile exists"
  573. (and (project-file-exists "Gemfile")
  574. (executable-find "bundle")))
  575. (defun should-run-docker-compose ()
  576. "Determines if docker-compose should be used."
  577. (and (project-file-exists "docker-compose.yml")
  578. feature-use-docker-compose))
  579. (defun construct-cucumber-command (command-template opts-str feature-arg)
  580. "Creates a complete command to launch cucumber"
  581. (let ((base-command
  582. (concat (replace-regexp-in-string
  583. "{options}" opts-str
  584. (replace-regexp-in-string "{feature}"
  585. (if (should-run-docker-compose) (replace-regexp-in-string (feature-project-root) "" feature-arg) feature-arg)
  586. command-template) t t))))
  587. (concat (if (should-run-docker-compose) (concat feature-docker-compose-command " run " feature-docker-compose-container " ") "")
  588. (concat (if (can-run-bundle) "bundle exec " "")
  589. base-command))))
  590. (defun* feature-run-cucumber (cuke-opts &key feature-file)
  591. "Runs cucumber with the specified options"
  592. (feature-register-verify-redo (list 'feature-run-cucumber
  593. (list 'quote cuke-opts)
  594. :feature-file feature-file))
  595. ;; redoer is registered
  596. (let ((opts-str (mapconcat 'identity cuke-opts " "))
  597. (feature-arg (if feature-file
  598. feature-file
  599. feature-default-directory))
  600. (command-template (if (project-file-exists "Rakefile")
  601. feature-rake-command
  602. feature-cucumber-command)))
  603. (ansi-color-for-comint-mode-on)
  604. (let ((default-directory (feature-project-root))
  605. (compilation-scroll-output t))
  606. (if feature-use-rvm
  607. (rvm-activate-corresponding-ruby))
  608. (if feature-use-chruby
  609. (chruby-use-corresponding))
  610. (compile (construct-cucumber-command command-template opts-str feature-arg) t))))
  611. (defun feature-root-directory-p (a-directory)
  612. "Tests if a-directory is the root of the directory tree (i.e. is it '/' on unix)."
  613. (equal a-directory (file-name-directory (directory-file-name a-directory))))
  614. (defun feature-project-root (&optional directory)
  615. "Finds the root directory of the project by walking the directory tree until it finds the file set by `feature-root-marker-file-name' (presumably, application root)"
  616. (let ((directory (file-name-as-directory (or directory default-directory))))
  617. (if (feature-root-directory-p directory)
  618. (error (concat "Could not find " feature-root-marker-file-name)))
  619. (if (file-exists-p (concat directory feature-root-marker-file-name))
  620. directory
  621. (feature-project-root (file-name-directory (directory-file-name directory))))))
  622. (defun expand-home-shellism ()
  623. (replace-regexp-in-string "~" "$HOME" (feature-project-root))
  624. )
  625. (defun feature-find-step-definition (action)
  626. "Find the step-definition under (point). Requires ruby."
  627. (let* ((root (feature-project-root))
  628. (input (thing-at-point 'line))
  629. (_ (set-text-properties 0 (length input) nil input))
  630. (result (shell-command-to-string (format "cd %S && %s %S/find_step.rb %s %s %S %s %s"
  631. (expand-home-shellism)
  632. feature-ruby-command
  633. feature-support-directory
  634. (feature-detect-language)
  635. (buffer-file-name)
  636. (line-number-at-pos)
  637. (shell-quote-argument feature-step-search-path)
  638. (shell-quote-argument feature-step-search-gems-path))))
  639. (matches (read result))
  640. (matches-length (safe-length matches)))
  641. (if (listp matches)
  642. (if (> matches-length 0)
  643. (let* ((file-and-line (if (= matches-length 1)
  644. (cdr (car matches))
  645. (cdr (assoc (ido-completing-read "Which example needed? " (mapcar (lambda (pair) (car pair)) matches)) matches))))
  646. (matched? (string-match "^\\(.+\\):\\([0-9]+\\)$" file-and-line)))
  647. (if matched?
  648. (let ((file (format "%s/%s" root (match-string 1 file-and-line)))
  649. (line-no (string-to-number (match-string 2 file-and-line))))
  650. (funcall action root file line-no))
  651. (message "An error occured: \n%s" result)))
  652. (message "No matching steps found for:\n%s" input))
  653. (message "An error occured: \n%s" result))))
  654. (defun feature-goto-step-definition ()
  655. "Goto the step-definition under (point). Requires ruby."
  656. (interactive)
  657. (feature-find-step-definition
  658. (lambda (project-root file line-no)
  659. (progn
  660. (ring-insert find-tag-marker-ring (point-marker))
  661. (find-file file)
  662. (goto-char (point-min))
  663. (forward-line (1- line-no))))))
  664. (provide 'cucumber-mode)
  665. (provide 'feature-mode)
  666. ;;; feature-mode.el ends here