Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
675 lines (577 sloc) 24.1 KB
;;; rails-core.el --- core helper functions and macros for emacs-rails
;; Copyright (C) 2006 Dmitry Galinsky <dima dot exe at gmail dot com>
;; Authors: Dmitry Galinsky <dima dot exe at gmail dot com>,
;; Rezikov Peter <crazypit13 (at)>
;; Keywords: ruby rails languages oop
;; $URL$
;; $Id$
;;; License
;; 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
;; 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
(require 'rails-lib))
(defvar rails-core:class-dirs
"Directories with Rails classes")
(defun rails-core:class-by-file (filename)
"Return the class associated with FILENAME.
--> Foo::BarBaz"
(let* ((case-fold-search nil)
(path (replace-regexp-in-string
(strings-join "\\|" rails-core:class-dirs)) "\\3" filename))
(path (replace-regexp-in-string "/" " " path))
(path (replace-regexp-in-string "_" " " path)))
" " ""
" " "::"
(if (string-match "^ *\\([0-9]+ *\\)?[A-Z]" path)
(capitalize path))))))
(defun rails-core:file-by-class (classname &optional do-not-append-ext)
"Return the filename associated with CLASSNAME.
If the optional parameter DO-NOT-APPEND-EXT is set this function
will not append \".rb\" to result."
(concat (decamelize (replace-regexp-in-string "::" "/" classname))
(unless do-not-append-ext ".rb")))
;;;;;;;;;; Files ;;;;;;;;;;
(defun rails-core:file (file-name)
"Return the full path for FILE-NAME in a Rails directory."
(when file-name
(if (file-name-absolute-p file-name)
(concat root file-name)))))
(defun rails-core:quoted-file (file-name)
"Return the quoted full path for FILE-NAME in a Rails directory."
(concat "\"" (rails-core:file file-name) "\""))
(defun rails-core:find-file (file-name)
"Open the file named FILE_NAME in a Rails directory."
(when-bind (file (rails-core:file file-name))
(find-file file)))
(defun rails-core:find-file-if-exist (file-name)
"Open the file named FILE-NAME in a Rails directory only if the file exists."
(let ((file-name (rails-core:file file-name)))
(when (file-exists-p file-name)
(find-file file-name))))
(defun rails-core:find-or-ask-to-create (question file)
"Open the file named FILE in a Rails directory if it exists. If
it does not exist, ask to create it using QUESTION as a prompt."
(find-or-ask-to-create question (rails-core:file file)))
(defun rails-core:strip-namespace (class-name)
"Strip namespace of CLASS-NAME, eg Foo::Bar -> Bar."
(let ((name-list (split-string class-name "::")))
(car (last name-list))))
;; Funtions, that retrun Rails objects full pathes
(defun rails-core:model-file (model-name)
"Return the model file from the model name."
(when model-name
(let* ((stripped-model-file
(rails-core:strip-namespace model-name)))
(rails-core:file-by-class model-name)))
(rails-core:file (concat "app/models/" model-file)))
(concat "app/models/" model-file))
(rails-core:file (concat "app/models/" stripped-model-file)))
(concat "app/models/" stripped-model-file))
(t nil)))))
(defun rails-core:model-exist-p (model-name)
"Return t if model MODEL-NAME exist."
(let ((model-file (rails-core:model-file model-name)))
(when model-file
(and (file-exists-p (rails-core:file model-file))
(not (rails-core:observer-p model-name))
(not (rails-core:mailer-p model-name))))))
(defun rails-core:controller-file (controller-name)
"Return the path to the controller CONTROLLER-NAME."
(when controller-name
(concat "app/controllers/"
(rails-core:short-controller-name controller-name) t)
(unless (string-equal controller-name "Application") "_controller")
(defun rails-core:controller-exist-p (controller-name)
"Return t if controller CONTROLLER-NAME exist."
(when controller-name
(rails-core:controller-file controller-name)))))
(defun rails-core:controller-file-by-model (model)
(when model
(let* ((controller (pluralize-string model)))
;(controller (when controller (capitalize controller))))
(setq controller
((rails-core:controller-exist-p controller) controller) ;; pluralized
((rails-core:controller-exist-p model) model) ;; singularized
(t (let ((controllers (rails-core:controllers t)))
;; with namespace
(list controller model)
:test #'(lambda(x y)
(string= (car x) (rails-core:strip-namespace y))
(string= (cadr x) (rails-core:strip-namespace y)))))))))))
(when controller
(rails-core:controller-file controller)))))
(defun rails-core:observer-file (observer-name)
"Return the path to the observer OBSERVER-NAME."
(when observer-name
(rails-core:model-file (concat observer-name "Observer"))))
(defun rails-core:mailer-file (mailer)
(when (and mailer
(rails-core:mailer-p mailer))
(rails-core:model-file mailer)))
(defun rails-core:mailer-exist-p (mailer)
(when mailer
(file-exists-p (rails-core:file (rails-core:mailer-file mailer)))))
(defun rails-core:migration-file (migration-name)
"Return the model file from the MIGRATION-NAME."
(when migration-name
(let ((dir "db/migrate/")
(name (replace-regexp-in-string
" " "_"
(rails-core:file-by-class migration-name))))
(when (string-match "^[^0-9]+[^_]" name) ; try search when the name without migration number
(let ((files (directory-files (rails-core:file dir)
(concat "[0-9]+_" name "$"))))
(setq name (if files
(car files)
(when name
(concat dir name)))))
(defun rails-core:migration-file-by-model (model)
(when model
(concat "Create" (rails-core:class-by-file (pluralize-string model))))))
(defun rails-core:model-by-migration-filename (migration-filename)
(when migration-filename
(let ((model-name (singularize-string
(string=~ "[0-9]+_create_\\(\\w+\\)\.rb" (buffer-name) $1))))
(when (and model-name
(rails-core:model-exist-p model-name))
(defun rails-core:configuration-file (file)
"Return the path to the configuration FILE."
(when file
(concat "config/" file)))
(defun rails-core:plugin-file (plugin file)
"Return the path to the FILE in Rails PLUGIN."
(concat "vendor/plugins/" plugin "/" file))
(defun rails-core:layout-file (layout)
"Return the path to the layout file named LAYOUT."
(let ((its rails-templates-list)
(while (and (car its)
(not filename))
(when (file-exists-p (format "%sapp/views/layouts/%s.%s" (rails-project:root) layout (car its)))
(setq filename (format "app/views/layouts/%s.%s" layout (car its))))
(setq its (cdr its)))
(defun rails-core:js-file (js)
"Return the path to the JavaScript file named JS."
(concat "public/javascripts/" js ".js"))
(defun rails-core:partial-name (name)
"Return the file name of partial NAME."
(if (string-match "/" name)
(concat "app/views/"
(replace-regexp-in-string "\\([^/]*\\)$" "_\\1.rhtml" name))
(concat (rails-core:views-dir (rails-core:current-controller))
"_" name ".rhtml")))
(defun rails-core:view-name (name)
"Return the file name of view NAME."
(concat (rails-core:views-dir (rails-core:current-controller))
name ".rhtml")) ;; BUG: will fix it
(defun rails-core:helper-file (controller)
"Return the helper file name for the controller named
(if (string= "Test/TestHelper" controller)
(rails-core:file (rails-core:file-by-class "Test/TestHelper"))
(when controller
(format "app/helpers/%s_helper.rb"
(replace-regexp-in-string "_controller" ""
(rails-core:file-by-class controller t))))))
(defun rails-core:functional-test-file (controller)
"Return the functional test file name for the controller named
(when controller
(format "test/functional/%s_test.rb"
(rails-core:file-by-class (rails-core:long-controller-name controller) t))))
(defun rails-core:unit-test-file (model)
"Return the unit test file name for the model named MODEL."
(when model
(format "test/unit/%s_test.rb" (rails-core:file-by-class model t))))
(defun rails-core:unit-test-exist-p (model)
"Return the unit test file name for the model named MODEL."
(let ((test (rails-core:unit-test-file model)))
(when test
(file-exists-p (rails-core:file test)))))
(defun rails-core:fixture-file (model)
"Return the fixtures file name for the model named MODEL."
(when model
(format "test/fixtures/%s.yml" (pluralize-string (rails-core:file-by-class model t)))))
(defun rails-core:fixture-exist-p (model)
(when model
(rails-core:file (rails-core:fixture-file model)))))
(defun rails-core:views-dir (controller)
"Return the view directory name for the controller named CONTROLLER."
(format "app/views/%s/" (replace-regexp-in-string "_controller" "" (rails-core:file-by-class controller t))))
(defun rails-core:stylesheet-name (name)
"Return the file name of the stylesheet named NAME."
(concat "public/stylesheets/" name ".css"))
(defun rails-core:controller-name (controller-file)
"Return the class name of the controller named CONTROLLER.
Bar in Foo dir -> Foo::Bar"
(if (eq (elt controller-file 0) 47) ;;; 47 == '/'
(subseq controller-file 1)
(let ((current-controller (rails-core:current-controller)))
(if (string-match ":" current-controller)
(concat (replace-regexp-in-string "[^:]*$" "" current-controller)
(defun rails-core:short-controller-name (controller)
"Convert FooController -> Foo."
(remove-postfix controller "Controller" ))
(defun rails-core:long-controller-name (controller)
"Convert Foo/FooController -> FooController."
(if (string-match "Controller$" controller)
(concat controller "Controller")))
;;;;;;;;;; Functions that return collection of Rails objects ;;;;;;;;;;
(defun rails-core:observer-p (name)
(when name
(if (string-match "\\(Observer\\|_observer\\)\\(\\.rb\\)?$" name)
t nil)))
(defun rails-core:mailer-p (name)
(when name
(if (string-match "\\(Mailer\\|Notifier\\|_mailer\\|_notifier\\)\\(\\.rb\\)?$" name)
t nil)))
(defun rails-core:controllers (&optional cut-contoller-suffix)
"Return a list of Rails controllers. Remove the '_controller'
suffix if CUT-CONTOLLER-SUFFIX is non nil."
#'(lambda (controller)
(if cut-contoller-suffix
(replace-regexp-in-string "_controller\\." "." controller)
#'(lambda (controller)
(string-match "\\(application\\|[a-z0-9_]+_controller\\)\\.rb$"
(find-recursive-files "\\.rb$" (rails-core:file "app/controllers/")))))
(defun rails-core:functional-tests ()
"Return a list of Rails functional tests."
(remove-postfix (rails-core:class-by-file it)
(find-recursive-files "\\.rb$" (rails-core:file "test/functional/"))))
(defun rails-core:models ()
"Return a list of Rails models."
#'(lambda (file) (or (rails-core:observer-p file)
(rails-core:mailer-p file)))
(find-recursive-files "\\.rb$" (rails-core:file "app/models/")))))
(defun rails-core:unit-tests ()
"Return a list of Rails functional tests."
(remove-postfix (rails-core:class-by-file it)
(find-recursive-files "\\.rb$" (rails-core:file "test/unit/"))))
(defun rails-core:observers ()
"Return a list of Rails observers."
#'(lambda (observer) (replace-regexp-in-string "Observer$" "" observer))
(find-recursive-files "\\(_observer\\)\\.rb$" (rails-core:file "app/models/")))))
(defun rails-core:mailers ()
"Return a list of Rails mailers."
(find-recursive-files "\\(_mailer\\|_notifier\\)\\.rb$" (rails-core:file "app/models/"))))
(defun rails-core:helpers ()
"Return a list of Rails helpers."
#'(lambda (helper) (replace-regexp-in-string "Helper$" "" helper))
(find-recursive-files "_helper\\.rb$" (rails-core:file "app/helpers/"))))
(list "Test/TestHelper")))
(defun rails-core:migrations (&optional strip-numbers)
"Return a list of Rails migrations."
(let (migrations)
#'(lambda (migration)
(replace-regexp-in-string "^\\([0-9]+\\)" "\\1 " migration))
(find-recursive-files "^[0-9]+_.*\\.rb$" (rails-core:file "db/migrate/"))))))
(if strip-numbers
(mapcar #'(lambda(i) (car (last (split-string i " "))))
(defun rails-core:migration-versions (&optional with-zero)
"Return a list of migtaion versions as the list of strings. If
second argument WITH-ZERO is present, append the \"000\" version
of migration."
(let ((ver (mapcar
#'(lambda(it) (car (split-string it " ")))
(if with-zero
(append ver '("000"))
(defun rails-core:plugins ()
"Return a list of Rails plugins."
(directory-files (rails-core:file "vendor/plugins") t "^[^\\.]"))))
(defun rails-core:plugin-files (plugin)
"Return a list of files in specific Rails plugin."
(find-recursive-files "^[^.]" (rails-core:file (concat "vendor/plugins/" plugin))))
(defun rails-core:layouts ()
"Return a list of Rails layouts."
#'(lambda (l)
(replace-regexp-in-string "\\.[^.]+$" "" l))
(find-recursive-files (rails-core:regex-for-match-view) (rails-core:file "app/views/layouts"))))
(defun rails-core:fixtures ()
"Return a list of Rails fixtures."
#'(lambda (l)
(replace-regexp-in-string "\\.[^.]+$" "" l))
(find-recursive-files "\\.yml$" (rails-core:file "test/fixtures/"))))
(defun rails-core:configuration-files ()
"Return a files of files from config folder."
(find-recursive-files nil (rails-core:file "config/")))
(defun rails-core:regex-for-match-view ()
"Return a regex to match Rails view templates.
The file extensions used for views are defined in `rails-templates-list'."
(format "\\.\\(%s\\)$" (strings-join "\\|" rails-templates-list)))
(defun rails-core:get-view-files (controller-class &optional action)
"Retun a list containing the view file for CONTROLLER-CLASS#ACTION.
If the action is nil, return all views for the controller."
(rails-core:short-controller-name controller-class))) t
(if action
(concat "^" action (rails-core:regex-for-match-view))
(defun rails-core:extract-ancestors (classes)
"Return the parent classes from a list of classes named CLASSES."
(delete ""
(mapcar (lambda (class)
"::[^:]*$" "::"
(replace-regexp-in-string "^[^:]*$" "" class)))
(defun rails-core:models-ancestors ()
"Return the parent classes of models."
(rails-core:extract-ancestors (rails-core:models)))
(defun rails-core:controllers-ancestors ()
"Return the parent classes of controllers."
(rails-core:extract-ancestors (rails-core:controllers)))
;;;;;;;;;; Getting Controllers/Model/Action from current buffer ;;;;;;;;;;
(defun rails-core:current-controller ()
"Return the current Rails controller."
(let* ((file-class (rails-core:class-by-file (buffer-file-name))))
(unless (rails-core:mailer-p file-class)
(case (rails-core:buffer-type)
(:controller (rails-core:short-controller-name file-class))
(:view (rails-core:class-by-file
(directory-file-name (directory-of-file (buffer-file-name)))))
(:helper (remove-postfix file-class "Helper"))
(:functional-test (remove-postfix file-class "ControllerTest"))))))
(defun rails-core:current-model ()
"Return the current Rails model."
(let* ((file-class (rails-core:class-by-file (buffer-file-name))))
(unless (rails-core:mailer-p file-class)
(case (rails-core:buffer-type)
(:migration (rails-core:model-by-migration-filename (buffer-name)))
(:model file-class)
(:unit-test (remove-postfix file-class "Test"))
(:fixture (singularize-string file-class))))))
(defun rails-core:current-mailer ()
"Return the current Rails Mailer, else return nil."
(let* ((file-class (rails-core:class-by-file (buffer-file-name)))
(test (remove-postfix file-class "Test")))
(when (or (rails-core:mailer-p file-class)
(rails-core:mailer-p test))
(case (rails-core:buffer-type)
(:mailer file-class)
(:unit-test test)
(:view (rails-core:class-by-file
(directory-file-name (directory-of-file (buffer-file-name)))))))))
(defun rails-core:current-action ()
"Return the current action in the current Rails controller."
(case (rails-core:buffer-type)
(:controller (rails-core:current-method-name))
(:mailer (rails-core:current-method-name))
(:view (string-match "/\\([a-z0-9_]+\\)\.[a-z]+$" (buffer-file-name))
(match-string 1 (buffer-file-name)))))
(defun rails-core:current-helper ()
"Return the current helper"
(defun rails-core:current-plugin ()
"Return the current plugin name."
(let ((name (buffer-file-name)))
(when (string-match "vendor\\/plugins\\/\\([^\\/]+\\)" name)
(match-string 1 name))))
(defun rails-core:current-method-name ()
(when (search-backward-regexp "^[ ]*def \\([a-z0-9_]+\\)" nil t)
(match-string-no-properties 1))))
;;;;;;;;;; Determination of buffer type ;;;;;;;;;;
(defun rails-core:buffer-file-match (regexp)
"Match the current buffer file name to RAILS_ROOT + REGEXP."
(when-bind (file (rails-core:file regexp))
(string-match file
(buffer-file-name (current-buffer)))))
(defun rails-core:buffer-type ()
"Return the type of the current Rails file or nil if the type
cannot be determinated."
(loop for (type dir func) in rails-directory<-->types
when (and (rails-core:buffer-file-match dir)
(if func
(apply func (list (buffer-file-name (current-buffer))))
do (return type)))
;;;;;;;;;; Rails minor mode Buttons ;;;;;;;;;;
(define-button-type 'rails-button
'follow-link t
'action #'rails-core:button-action)
(defun rails-core:button-action (button)
(let* ((file-name (button-get button :rails:file-name))
(line-number (button-get button :rails:line-number))
(file (rails-core:file file-name)))
(when (and file
(file-exists-p file))
(find-file-other-window file)
(when line-number
(goto-line line-number)))))
;;;;;;;;;; Rails minor mode logs ;;;;;;;;;;
(defun rails-log-add (message)
"Add MESSAGE to the Rails minor mode log in RAILS_ROOT."
(append-string-to-file (rails-core:file "log/rails-minor-mode.log")
(format "%s: %s\n"
(format-time-string "%Y/%m/%d %H:%M:%S") message))))
(defun rails-logged-shell-command (command buffer)
"Execute a shell command in the buffer and write the results to
the Rails minor mode log."
(shell-command (format "%s %s" rails-ruby-command command) buffer)
(format "\n%s> %s\n%s" (rails-project:name)
command (buffer-string-by-name buffer))))
;;;;;;;;;; Rails menu ;;;;;;;;;;
(defun rails-core:menu-separator ()
(unless (rails-use-text-menu) 'menu (list "--" "--")))
(if (fboundp 'completion-posn-at-point-as-event)
(defun rails-core:menu-position ()
(completion-posn-at-point-as-event nil nil nil (+ (frame-char-height) 2)))
(defun rails-core:menu-position ()
(list '(300 50) (get-buffer-window (current-buffer)))))
(defun rails-core:menu (menu)
"Show a menu."
(let ((result
(if (rails-use-text-menu)
(tmm-prompt menu)
(x-popup-menu (rails-core:menu-position)
(rails-core:prepare-menu menu)))))
(if (listp result)
(first result)
(defvar rails-core:menu-letters-list
(let ((res '()))
(loop for i from (string-to-char "1") upto (string-to-char "9")
do (add-to-list 'res (char-to-string i) t))
(loop for i from (string-to-char "a") upto (string-to-char "z")
do (add-to-list 'res (char-to-string i) t))
"List contains 0-9a-z letter")
(defun rails-core:prepare-menu (menu)
"Append a prefix to each label of menu-item from MENU."
(let ((title (car menu))
(menu (cdr menu))
(result '())
(result-line '())
(letter 0))
(dolist (line menu)
(setq result-line '())
(dolist (it line)
(typecase it
(if (and (string= (car (rails-core:menu-separator)) (car it))
(string= (cadr (rails-core:menu-separator)) (cadr it)))
(add-to-list 'result-line it t)
(add-to-list 'result-line (cons
(format "%s) %s"
(nth letter rails-core:menu-letters-list)
(car it))
(cdr it))
(setq letter (+ 1 letter)))))
(add-to-list 'result-line it t))))
(add-to-list 'result result-line t))
(cons title result)))
;;;;;;;;;; Misc ;;;;;;;;;;
(defun rails-core:erb-block-string ()
"Return the contents of the current ERb block."
(let ((start (point)))
(search-backward-regexp "<%[=]?")
(let ((from (match-end 0)))
(search-forward "%>")
(let ((to (match-beginning 0)))
(when (>= to start)
(buffer-substring-no-properties from to))))))))
(defun rails-core:rhtml-buffer-p ()
"Return non nil if the current buffer is rhtml file."
(string-match "\\.rhtml$" (buffer-file-name)))
(provide 'rails-core)