;;; feature-mode.el --- Major mode for editing Gherkin (i.e. Cucumber) user stories ;;; Version: 0.4 ;;; Author: Michael Klishin ;;; URL: https://github.com/michaelklishin/cucumber.el ;;; Uploader: Kao Félix ;; Copyright (C) 2008 — 2012 Michael Klishin and other contributors ;; ;; This program is free software; you can redistribute it and/or ;; modify it under the terms of the GNU General Public License ;; as published by the Free Software Foundation; either version 2 ;; of the License, or (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program; if not, write to the Free Software ;; Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110. ;; ;; Copy files to ~/.emacs.d/elisp/feature-mode and add this to your ;; .emacs to load the mode ;; (add-to-list 'load-path "~/.emacs.d/elisp/feature-mode") ;; ;; optional configurations ;; ;; default language if .feature doesn't have "# language: fi" ;; ;(setq feature-default-language "fi") ;; ;; point to cucumber languages.yml or gherkin i18n.yml to use ;; ;; exactly the same localization your cucumber uses ;; ;(setq feature-default-i18n-file "/path/to/gherkin/gem/i18n.yml") ;; ;; and load it ;; (require 'feature-mode) ;; (add-to-list 'auto-mode-alist '("\.feature$" . feature-mode)) ;; ;; If using RVM, set `feature-use-rvm' to `t' to enable RVM ;; support. This requires `rvm.el'. ;; ;; If using Chruby, set `feature-use-chruby' to `t' to enable ;; Chruby support. This requires `chruby.el' ;; ;; Language used in feature file is automatically detected from ;; "language: [2-letter ISO-code]" tag in feature file. You can ;; choose the language feature-mode should use in case autodetection ;; fails. Just add ;; (setq feature-default-language "en") ;; to your .emacs ;; ;; Translations are loaded from ~/.emacs.d/elisp/feature-mode/i18n.yml ;; by default. You can configure feature-mode to load translations ;; directly from cucumber languages.yml or gherkin i18n.yml. Just add ;; (setq feature-default-i18n-file ;; "/usr/lib/ruby/gems/1.8/gems/cucumber-0.4.4/lib/cucumber/languages.yml") ;; to your .emacs before ;; (require 'feature-mode) ;; ;; ;; In order to get goto-step-definition to work, you must install the ;; ruby_parser gem (version 2.0.x). For example: ;; ;; gem install ruby_parser --version=2.0.5 ;; ;; (be sure and use the ruby-interpreter that emacs will use based on ;; `exec-path') ;; ;; ;; Key Bindings ;; ------------ ;; ;; \C-c ,v ;; : Verify all scenarios in the current buffer file. ;; ;; \C-c ,s ;; : Verify the scenario under the point in the current buffer. ;; ;; \C-c ,f ;; : Verify all features in project. (Available in feature and ;; ruby files) ;; ;; \C-c ,r ;; : Repeat the last verification process. ;; ;; \C-c ,g ;; : Go to step-definition under point (eval-when-compile (require 'cl)) (require 'thingatpt) (require 'etags) (defcustom feature-cucumber-command "cucumber {options} \"{feature}\"" "command used to run cucumber when there is no Rakefile" :group 'feature-mode :type 'string) (defcustom feature-rake-command "rake cucumber CUCUMBER_OPTS=\"{options}\" FEATURE=\"{feature}\"" "command used to run cucumber when there is a Rakefile" :group 'feature-mode :type 'string) (defcustom feature-enable-back-denting t "when enabled, subsequent pressing the tab key back-dents the current line by `feature-indent-offset' spaces" :type 'boolean :group 'feature-mode) (defcustom feature-use-rvm nil "t when RVM is in use. (Requires rvm.el)" :type 'boolean :group 'feature-mode) (defcustom feature-use-chruby nil "t when Chruby is in use. (Requires chruby.el)" :type 'boolean :group 'feature-mode) (defcustom feature-root-marker-file-name "features" "file to look for to find the project root." :group 'feature-mode :type 'string) (defcustom feature-align-steps-after-first-word nil "when set to t, make step lines align on the space after the first word" :type 'boolean :group 'feature-mode) (defcustom feature-step-search-path "features/**/*steps.rb" "Path to project step definitions" :type 'string :group 'feature-mode) (defcustom feature-step-search-gems-path "gems/ruby/*/gems/*/**/*steps.rb" "Path to find step definitions in installed gems" :type 'string :group 'feature-mode) (defcustom feature-ruby-command "ruby" "Command to run ruby" :type 'string :group 'feature-mode) ;; Docker related (defcustom feature-use-docker-compose t "Use docker-compose when docker-compose.yml exists in project." :type 'boolean :group 'feature-mode) (defcustom feature-docker-compose-command "docker-compose" "Command to run docker-compose." :type 'string :group 'feature-mode) (defcustom feature-docker-compose-container "app" "The container to run cucumber in." :type 'string :group 'feature-mode) ;; ;; Keywords and font locking ;; (when (featurep 'font-lock) (or (boundp 'font-lock-variable-name-face) (setq font-lock-variable-name-face font-lock-type-face))) (defun load-gherkin-i10n (filename) "Read and parse Gherkin l10n from given file." (interactive "Load l10n file: ") (with-temp-buffer (insert-file-contents filename) (parse-gherkin-l10n))) (defun parse-gherkin-l10n () (let (languages-alist) (save-excursion (goto-char (point-min)) (while (not (eobp)) (if (try-find-next-language) (let ((lang-beg (+ (point) 1)) (lang-end (progn (end-of-line) (- (point) 2))) (kwds-beg (+ (point) 1)) (kwds-end (progn (try-find-next-language) (point)))) (add-to-list 'languages-alist (cons (filter-buffer-substring lang-beg lang-end) (parse-gherkin-l10n-translations kwds-beg kwds-end))))))) (nreverse languages-alist))) (defun try-find-next (regexp) (let (search-result) (setq search-result (search-forward-regexp regexp nil t)) (if search-result (beginning-of-line) (goto-char (point-max))) search-result)) (defun try-find-next-language () (try-find-next "^\"[^\"]+\":")) (defun try-find-next-translation () (try-find-next "^ \\([^ :]+\\): +\"?\\*?|?\\([^\"\n]+\\)\"?")) (defun parse-gherkin-l10n-translations (beg end) (let (translations-alist) (save-excursion (save-restriction (narrow-to-region beg end) (goto-char (point-min)) (while (not (eobp)) (if (try-find-next-translation) (let ((kwname (match-string-no-properties 1)) (kw (match-string-no-properties 2))) (add-to-list 'translations-alist (cons (intern kwname) (if (or (equal kwname "name") (equal kwname "native")) kw (build-keyword-matcher kw)))))) (end-of-line)))) (nreverse translations-alist))) (defun build-keyword-matcher (keyword) (concat "^[ \t]*\\(" (replace-regexp-in-string "|" "\\\\|" keyword) "\\):?")) (defvar feature-default-language "en") (defvar feature-default-directory "features") (defvar feature-default-i18n-file (expand-file-name (concat (file-name-directory load-file-name) "/i18n.yml"))) (defconst feature-keywords-per-language (if (file-readable-p feature-default-i18n-file) (load-gherkin-i10n feature-default-i18n-file) '(("en" . ((feature . "^ *\\(Feature\\):") (background . "^ *\\(Background\\):") (scenario . "^ *\\(Scenario\\):") (scenario_outline . "^ *\\(Scenario Outline\\):") (given . "^ *\\(Given\\) ") (when . "^ *\\(When\\) ") (then . "^ *\\(Then\\) ") (but . "^ *\\(But\\) ") (and . "^ *\\(And\\) ") (examples . "^ *\\(Examples\\|Scenarios\\):")))))) (defconst feature-font-lock-keywords '((feature (0 font-lock-keyword-face) (".*" nil nil (0 font-lock-type-face t))) (background . (0 font-lock-keyword-face)) (scenario (0 font-lock-keyword-face) (".*" nil nil (0 font-lock-function-name-face nil))) (scenario_outline (0 font-lock-keyword-face) (".*" nil nil (0 font-lock-function-name-face t))) (given . font-lock-keyword-face) (when . font-lock-keyword-face) (then . font-lock-keyword-face) (but . font-lock-keyword-face) (and . font-lock-keyword-face) (examples . font-lock-keyword-face) ("<[^>]*>" . font-lock-variable-name-face) ("^ *@.*" . font-lock-preprocessor-face) ("^ *#.*" 0 font-lock-comment-face t))) ;; ;; Keymap ;; (defvar feature-mode-map nil "Keymap used in feature mode") (if feature-mode-map nil (setq feature-mode-map (make-sparse-keymap)) (define-key feature-mode-map "\C-m" 'newline) (define-key feature-mode-map (kbd "C-c ,s") 'feature-verify-scenario-at-pos) (define-key feature-mode-map (kbd "C-c ,v") 'feature-verify-all-scenarios-in-buffer) (define-key feature-mode-map (kbd "C-c ,f") 'feature-verify-all-scenarios-in-project) (define-key feature-mode-map (kbd "C-c ,g") 'feature-goto-step-definition) (define-key feature-mode-map (kbd "M-.") 'feature-goto-step-definition)) ;; Add relevant feature keybindings to ruby modes (add-hook 'ruby-mode-hook (lambda () (local-set-key (kbd "C-c ,f") 'feature-verify-all-scenarios-in-project))) ;; ;; Syntax table ;; (defvar feature-mode-syntax-table nil "Syntax table in use in ruby-mode buffers.") (unless feature-mode-syntax-table (setq feature-mode-syntax-table (make-syntax-table))) ;; Constants (defconst feature-blank-line-re "^[ \t]*\\(?:#.*\\)?$" "Regexp matching a line containing only whitespace.") (defconst feature-example-line-re "^[ \t]*|" "Regexp matching a line containing scenario example.") (defconst feature-tag-line-re "^[ \t]*@" "Regexp matching a tag/annotation") (defconst feature-pystring-re "^[ \t]*\"\"\"$" "Regexp matching a pystring") (defun feature-feature-re (language) (cdr (assoc 'feature (cdr (assoc language feature-keywords-per-language))))) (defun feature-scenario-re (language) (cdr (assoc 'scenario (cdr (assoc language feature-keywords-per-language))))) (defun feature-examples-re (language) (cdr (assoc 'examples (cdr (assoc language feature-keywords-per-language))))) (defun feature-background-re (language) (cdr (assoc 'background (cdr (assoc language feature-keywords-per-language))))) (defun feature-given-re (language) (cdr (assoc 'given (cdr (assoc language feature-keywords-per-language))))) (defun feature-when-re (language) (cdr (assoc 'when (cdr (assoc language feature-keywords-per-language))))) (defun feature-then-re (language) (cdr (assoc 'then (cdr (assoc language feature-keywords-per-language))))) (defun feature-and-re (language) (cdr (assoc 'and (cdr (assoc language feature-keywords-per-language))))) (defun feature-but-re (language) (cdr (assoc 'but (cdr (assoc language feature-keywords-per-language))))) ;; ;; Variables ;; (defvar feature-mode-hook nil "Hook run when entering `feature-mode'.") (defcustom feature-indent-initial-offset 0 "Indentation of the first file" :type 'integer :group 'feature-mode) (defcustom feature-indent-level 2 "Indentation of feature statements" :type 'integer :group 'feature-mode) (defcustom feature-indent-offset 2 "*Amount of offset per level of indentation." :type 'integer :group 'feature-mode) (defun given-when-then-wordlength (lang) (let* ((when-then-and-words '(given when then and but)) (language-keywords (cdr (assoc lang feature-keywords-per-language))) (rexes (append (mapcar (lambda (kw) (cdr (assoc kw language-keywords))) when-then-and-words)))) (beginning-of-line) ;; white-space means offset -1 (if (or (bobp) (eobp)) nil (if (looking-at feature-blank-line-re) 0 (if (some (lambda (rex) (looking-at rex)) rexes) (length (match-string 1)) nil))))) (defun compute-given-when-then-offset (lang) (if feature-align-steps-after-first-word (progn (setq current-word-length (given-when-then-wordlength lang)) (cond ;; a non-given-when-then-line doesn't adjust the ;; offset ((null current-word-length) 0) ;; the same happens for empty lines ((= 0 current-word-length) 0) ;; we are on a proper line, figure out ;; the lengths of all lines preceding us (t (let ((search (lambda (direction lang) (forward-line direction) (setq search-word-length (given-when-then-wordlength lang)) (cond ((null search-word-length) nil) (t (cons search-word-length (funcall search direction lang))))))) (setq previous-lengths (delq 0 (save-excursion (funcall search -1 lang)))) (if (not (null previous-lengths)) (- (car previous-lengths) current-word-length) 0))))) 0)) (defun feature-search-for-regex-match (key) "Search for matching regexp on each line" (forward-line -1) (while (and (not (funcall key)) (> (point) (point-min))) (forward-line -1)) ) (defun feature-compute-indentation () "Calculate the maximum sensible indentation for the current line." (save-excursion (beginning-of-line) (if (bobp) feature-indent-initial-offset (let* ((lang (feature-detect-language)) (given-when-then-offset (compute-given-when-then-offset lang)) (saved-indentation (current-indentation))) (cond ((looking-at (feature-feature-re lang)) (progn (feature-search-for-regex-match (lambda () (looking-at (feature-feature-re lang)))) (current-indentation) )) ((or (looking-at (feature-background-re lang)) (looking-at (feature-scenario-re lang)) (looking-at feature-tag-line-re)) (progn (feature-search-for-regex-match (lambda () (or (looking-at (feature-feature-re lang)) (looking-at feature-tag-line-re) (looking-at (feature-background-re lang)) (looking-at (feature-scenario-re lang))))) (cond ((or (looking-at (feature-feature-re lang)) (looking-at feature-tag-line-re) ) feature-indent-level) ((or (looking-at (feature-background-re lang)) (looking-at (feature-scenario-re lang)) ) (current-indentation)) (t saved-indentation)) )) ((looking-at (feature-examples-re lang)) (progn (feature-search-for-regex-match (lambda () (or (looking-at (feature-background-re lang)) (looking-at (feature-scenario-re lang))))) (if (or (looking-at (feature-background-re lang)) (looking-at (feature-scenario-re lang))) (+ (current-indentation) feature-indent-offset) saved-indentation) )) ((or (looking-at (feature-given-re lang)) (looking-at (feature-when-re lang)) (looking-at (feature-then-re lang)) (looking-at (feature-and-re lang)) (looking-at (feature-but-re lang))) (progn (feature-search-for-regex-match (lambda () (or (looking-at (feature-background-re lang)) (looking-at (feature-scenario-re lang)) (looking-at (feature-given-re lang)) (looking-at (feature-when-re lang)) (looking-at (feature-then-re lang)) (looking-at (feature-and-re lang)) (looking-at (feature-but-re lang))))) (cond ((or (looking-at (feature-background-re lang)) (looking-at (feature-scenario-re lang))) (+ (current-indentation) feature-indent-offset)) ((or (looking-at (feature-given-re lang)) (looking-at (feature-when-re lang)) (looking-at (feature-then-re lang)) (looking-at (feature-and-re lang)) (looking-at (feature-but-re lang))) (current-indentation)) (t saved-indentation)) )) ((or (looking-at feature-example-line-re) (looking-at feature-pystring-re)) (progn (feature-search-for-regex-match (lambda () (or (looking-at (feature-examples-re lang)) (looking-at (feature-given-re lang)) (looking-at (feature-when-re lang)) (looking-at (feature-then-re lang)) (looking-at (feature-and-re lang)) (looking-at (feature-but-re lang)) (looking-at feature-example-line-re)))) (cond ((or (looking-at (feature-examples-re lang)) (looking-at (feature-given-re lang)) (looking-at (feature-when-re lang)) (looking-at (feature-then-re lang)) (looking-at (feature-and-re lang)) (looking-at (feature-but-re lang))) (+ (current-indentation) feature-indent-offset)) ((or (looking-at feature-example-line-re) (looking-at feature-pystring-re)) (current-indentation)) (t saved-indentation)) )) (t (progn (feature-search-for-regex-match (lambda () (not (looking-at feature-blank-line-re)))) (+ (current-indentation) given-when-then-offset (if (or (looking-at (feature-feature-re lang)) (looking-at (feature-scenario-re lang)) (looking-at (feature-background-re lang))) feature-indent-offset 0)) )) ))))) (defun feature-indent-line () "Indent the current line. The first time this command is used, the line will be indented to the maximum sensible indentation. Each immediately subsequent usage will back-dent the line by `feature-indent-offset' spaces. On reaching column 0, it will cycle back to the maximum sensible indentation." (interactive "*") (let ((ci (current-indentation)) (cc (current-column)) (need (feature-compute-indentation))) (save-excursion (beginning-of-line) (delete-horizontal-space) (if (and (equal last-command this-command) (/= ci 0) feature-enable-back-denting (called-interactively-p 'any)) (indent-to (* (/ (- ci 1) feature-indent-offset) feature-indent-offset)) (indent-to need))) (if (< (current-column) (current-indentation)) (forward-to-indentation 0)))) (defadvice orgtbl-tab (before feature-indent-table-advice (&optional arg)) "Table org mode ignores our indentation, lets force it." (feature-indent-line)) (ad-activate 'orgtbl-tab) (defun feature-font-lock-keywords-for (language) (let ((result-keywords . ())) (dolist (pair feature-font-lock-keywords) (let* ((keyword (car pair)) (font-locking (cdr pair)) (language-keyword (cdr (assoc keyword (cdr (assoc language feature-keywords-per-language)))))) (push (cons (or language-keyword keyword) font-locking) result-keywords))) result-keywords)) (defun feature-detect-language () (save-excursion (goto-char (point-min)) (if (re-search-forward "language: \\([[:alpha:]-]+\\)" (line-end-position) t) (match-string 1) feature-default-language))) (defun feature-mode-variables () (set-syntax-table feature-mode-syntax-table) (when mode-require-final-newline (setq require-final-newline t)) (setq comment-start "# ") (setq comment-start-skip "#+ *") (setq comment-end "") (setq parse-sexp-ignore-comments t) (set (make-local-variable 'indent-tabs-mode) 'nil) (set (make-local-variable 'indent-line-function) 'feature-indent-line) (set (make-local-variable 'font-lock-defaults) (list (feature-font-lock-keywords-for (feature-detect-language)) nil nil)) (set (make-local-variable 'font-lock-keywords) (feature-font-lock-keywords-for (feature-detect-language))) (set (make-local-variable 'imenu-generic-expression) `(("Scenario:" ,(feature-scenario-name-re (feature-detect-language)) 3) ("Background:" ,(feature-background-re (feature-detect-language)) 1)))) (defun feature-minor-modes () "Enable/disable all minor modes for feature mode." (turn-on-orgtbl) (set (make-local-variable 'electric-indent-functions) (list (lambda (arg) 'no-indent)))) ;; ;; Mode function ;; ;;;###autoload (defun feature-mode() "Major mode for editing plain text stories" (interactive) (kill-all-local-variables) (use-local-map feature-mode-map) (setq mode-name "Feature") (setq major-mode 'feature-mode) (feature-mode-variables) (feature-minor-modes) (run-mode-hooks 'feature-mode-hook)) ;;;###autoload (add-to-list 'auto-mode-alist '("\\.feature\\'" . feature-mode)) ;; ;; Snippets ;; (defvar feature-snippet-directory (concat (file-name-directory load-file-name) "snippets") "Path to the feature-mode snippets. If the yasnippet library is loaded, snippets in this directory are loaded on startup. If nil, don't load snippets.") (defvar feature-support-directory (concat (file-name-directory load-file-name) "support") "Path to support folder The support folder contains a ruby script that takes a step as an argument, and outputs a list of all matching step definitions") (declare-function yas/load-directory "yasnippet" t) (when (and (featurep 'yasnippet) feature-snippet-directory (file-exists-p feature-snippet-directory)) (yas/load-directory feature-snippet-directory)) ;; ;; Verifying features ;; (defun feature-scenario-name-re (language) (concat (feature-scenario-re (feature-detect-language)) "\\( Outline:?\\)?[[:space:]]+\\(.*\\)$")) (defun feature-verify-scenario-at-pos (&optional pos) "Run the scenario defined at pos. If post is not specified the current buffer location will be used." (interactive) (feature-run-cucumber (list "-l" (number-to-string (line-number-at-pos))) :feature-file (buffer-file-name))) (defun feature-verify-all-scenarios-in-buffer () "Run all the scenarios defined in current buffer." (interactive) (feature-run-cucumber '() :feature-file (buffer-file-name))) (defun feature-verify-all-scenarios-in-project () "Run all the scenarios defined in current project." (interactive) (feature-run-cucumber '())) (defun feature-register-verify-redo (redoer) "Register a bit of code that will repeat a verification process" (let ((redoer-cmd (eval (list 'lambda () '(interactive) (list 'let (list (list `default-directory default-directory)) redoer))))) (global-set-key (kbd "C-c ,r") redoer-cmd))) (defun project-file-exists (filename) "Determines if the project has a file" (file-exists-p (concat (feature-project-root) filename))) (defun can-run-bundle () "Determines if bundler is installed and a Gemfile exists" (and (project-file-exists "Gemfile") (executable-find "bundle"))) (defun should-run-docker-compose () "Determines if docker-compose should be used." (and (project-file-exists "docker-compose.yml") feature-use-docker-compose)) (defun construct-cucumber-command (command-template opts-str feature-arg) "Creates a complete command to launch cucumber" (let ((base-command (concat (replace-regexp-in-string "{options}" opts-str (replace-regexp-in-string "{feature}" (if (should-run-docker-compose) (replace-regexp-in-string (feature-project-root) "" feature-arg) feature-arg) command-template) t t)))) (concat (if (should-run-docker-compose) (concat feature-docker-compose-command " run " feature-docker-compose-container " ") "") (concat (if (can-run-bundle) "bundle exec " "") base-command)))) (defun* feature-run-cucumber (cuke-opts &key feature-file) "Runs cucumber with the specified options" (feature-register-verify-redo (list 'feature-run-cucumber (list 'quote cuke-opts) :feature-file feature-file)) ;; redoer is registered (let ((opts-str (mapconcat 'identity cuke-opts " ")) (feature-arg (if feature-file feature-file feature-default-directory)) (command-template (if (project-file-exists "Rakefile") feature-rake-command feature-cucumber-command))) (ansi-color-for-comint-mode-on) (let ((default-directory (feature-project-root)) (compilation-scroll-output t)) (if feature-use-rvm (rvm-activate-corresponding-ruby)) (if feature-use-chruby (chruby-use-corresponding)) (compile (construct-cucumber-command command-template opts-str feature-arg) t)))) (defun feature-root-directory-p (a-directory) "Tests if a-directory is the root of the directory tree (i.e. is it '/' on unix)." (equal a-directory (file-name-directory (directory-file-name a-directory)))) (defun feature-project-root (&optional directory) "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)" (let ((directory (file-name-as-directory (or directory default-directory)))) (if (feature-root-directory-p directory) (error (concat "Could not find " feature-root-marker-file-name))) (if (file-exists-p (concat directory feature-root-marker-file-name)) directory (feature-project-root (file-name-directory (directory-file-name directory)))))) (defun expand-home-shellism () (replace-regexp-in-string "~" "$HOME" (feature-project-root)) ) (defun feature-find-step-definition (action) "Find the step-definition under (point). Requires ruby." (let* ((root (feature-project-root)) (input (thing-at-point 'line)) (_ (set-text-properties 0 (length input) nil input)) (result (shell-command-to-string (format "cd %S && %s %S/find_step.rb %s %s %S %s %s" (expand-home-shellism) feature-ruby-command feature-support-directory (feature-detect-language) (buffer-file-name) (line-number-at-pos) (shell-quote-argument feature-step-search-path) (shell-quote-argument feature-step-search-gems-path)))) (matches (read result)) (matches-length (safe-length matches))) (if (listp matches) (if (> matches-length 0) (let* ((file-and-line (if (= matches-length 1) (cdr (car matches)) (cdr (assoc (ido-completing-read "Which example needed? " (mapcar (lambda (pair) (car pair)) matches)) matches)))) (matched? (string-match "^\\(.+\\):\\([0-9]+\\)$" file-and-line))) (if matched? (let ((file (format "%s/%s" root (match-string 1 file-and-line))) (line-no (string-to-number (match-string 2 file-and-line)))) (funcall action root file line-no)) (message "An error occured: \n%s" result))) (message "No matching steps found for:\n%s" input)) (message "An error occured: \n%s" result)))) (defun feature-goto-step-definition () "Goto the step-definition under (point). Requires ruby." (interactive) (feature-find-step-definition (lambda (project-root file line-no) (progn (ring-insert find-tag-marker-ring (point-marker)) (find-file file) (goto-char (point-min)) (forward-line (1- line-no)))))) (provide 'cucumber-mode) (provide 'feature-mode) ;;; feature-mode.el ends here