;;; rails-core.el --- core helper functions and macros for emacs-rails ;; Copyright (C) 2006 Dmitry Galinsky ;; Authors: Dmitry Galinsky , ;; Rezikov Peter ;; Keywords: ruby rails languages oop ;; $URL: svn+ssh://rubyforge/var/svn/emacs-rails/trunk/rails-core.el $ ;; $Id: rails-core.el 186 2007-04-20 15:34:51Z dimaexe $ ;;; 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 ;; 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. (eval-when-compile (require 'rails-lib)) (defvar rails-core:class-dirs '("app/controllers" "app/views" "app/models" "app/helpers" "test/unit" "test/functional" "test/fixtures") "Directories with Rails classes") (defun rails-core:class-by-file (filename) "Return the class associated with FILENAME. /(app/models|app/controllers|app/helpers|test/unit|test/functional)/foo/bar_baz --> Foo::BarBaz" (let* ((case-fold-search nil) (path (replace-regexp-in-string (format "\\(.*\\(%s\\)/\\)?\\([^\.]+\\)\\(.*\\)?" (strings-join "\\|" rails-core:class-dirs)) "\\3" filename)) (path (replace-regexp-in-string "/" " " path)) (path (replace-regexp-in-string "_" " " path))) (replace-regexp-in-string " " "" (replace-regexp-in-string " " "::" (if (string-match "^ *\\([0-9]+ *\\)?[A-Z]" path) 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." (let* ((case-fold-search nil) (path (replace-regexp-in-string "::" "/" classname)) (path (replace-regexp-in-string "\\([A-Z]+\\)\\([A-Z][a-z]\\)" "\\1_\\2" path)) (path (replace-regexp-in-string "\\([a-z\\d]\\)\\([A-Z]\\)" "\\1_\\2" path))) (concat (downcase path) (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) file-name (rails-project:with-root (root) (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))) ;; 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 (concat "app/models/" (rails-core:file-by-class model-name)))) (defun rails-core:model-exist-p (model-name) "Return t if controller CONTROLLER-NAME exist." (when model-name (and (file-exists-p (rails-core:file (rails-core:model-file model-name))) (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:file-by-class (rails-core:short-controller-name controller-name) t) (unless (string-equal controller-name "Application") "_controller") ".rb"))) (defun rails-core:controller-exist-p (controller-name) "Return t if controller CONTROLLER-NAME exist." (when controller-name (file-exists-p (rails-core:file (rails-core:controller-file controller-name))))) (defun rails-core:controller-file-by-model (model) (when model (let ((controller (pluralize-string model))) (when (rails-core:controller-exist-p 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) nil (concat "[0-9]+_" name "$")))) (setq name (if files (car files) nil)))) (when name (concat dir name))))) (defun rails-core:migration-file-by-model (model) (when model (rails-core:migration-file (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)) model-name)))) (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) filename) (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))) filename)) (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 CONTROLLER." (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 CONTROLLER." (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 (file-exists-p (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" (rails-core:class-by-file (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) controller-file) controller-file))))) (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) 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." (mapcar #'(lambda (controller) (rails-core:class-by-file (if cut-contoller-suffix (replace-regexp-in-string "_controller\\." "." controller) controller))) (delete-if-not #'(lambda (controller) (string-match "\\(application\\|[a-z0-9_]+_controller\\)\\.rb$" controller)) (find-recursive-files "\\.rb$" (rails-core:file "app/controllers/"))))) (defun rails-core:functional-tests () "Return a list of Rails functional tests." (mapcar #'(lambda(it) (remove-postfix (rails-core:class-by-file it) "ControllerTest")) (find-recursive-files "\\.rb$" (rails-core:file "test/functional/")))) (defun rails-core:models () "Return a list of Rails models." (mapcar #'rails-core:class-by-file (delete-if #'(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." (mapcar #'(lambda(it) (remove-postfix (rails-core:class-by-file it) "Test")) (find-recursive-files "\\.rb$" (rails-core:file "test/unit/")))) (defun rails-core:observers () "Return a list of Rails observers." (mapcar #'(lambda (observer) (replace-regexp-in-string "Observer$" "" observer)) (mapcar #'rails-core:class-by-file (find-recursive-files "\\(_observer\\)\\.rb$" (rails-core:file "app/models/"))))) (defun rails-core:mailers () "Return a list of Rails mailers." (mapcar #'rails-core:class-by-file (find-recursive-files "\\(_mailer\\|_notifier\\)\\.rb$" (rails-core:file "app/models/")))) (defun rails-core:helpers () "Return a list of Rails helpers." (append (mapcar #'(lambda (helper) (replace-regexp-in-string "Helper$" "" helper)) (mapcar #'rails-core:class-by-file (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) (setq migrations (reverse (mapcar #'(lambda (migration) (replace-regexp-in-string "^\\([0-9]+\\)" "\\1 " migration)) (mapcar #'rails-core:class-by-file (find-recursive-files "^[0-9]+_.*\\.rb$" (rails-core:file "db/migrate/")))))) (if strip-numbers (mapcar #'(lambda(i) (car (last (split-string i " ")))) migrations) migrations))) (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 " "))) (rails-core:migrations)))) (if with-zero (append ver '("000")) ver))) (defun rails-core:plugins () "Return a list of Rails plugins." (mapcar #'file-name-nondirectory (delete-if-not #'file-directory-p (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." (mapcar #'(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." (mapcar #'(lambda (l) (replace-regexp-in-string "\\.[^.]+$" "" l)) (find-recursive-files "\\.yml$" (rails-core:file "test/fixtures/")))) (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-project:with-root (root) (directory-files (rails-core:file (rails-core:views-dir (rails-core:short-controller-name controller-class))) t (if action (concat "^" action (rails-core:regex-for-match-view)) (rails-core:regex-for-match-view))))) (defun rails-core:extract-ancestors (classes) "Return the parent classes from a list of classes named CLASSES." (delete "" (uniq-list (mapcar (lambda (class) (replace-regexp-in-string "::[^:]*$" "::" (replace-regexp-in-string "^[^:]*$" "" class))) classes)))) (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" (rails-core:current-controller)) (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 () (save-excursion (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)))) t)) 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." (rails-project:with-root (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) (rails-log-add (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) 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)) res) "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 (cons (rails-core:menu-separator) (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) (progn (add-to-list 'result-line (cons (format "%s) %s" (nth letter rails-core:menu-letters-list) (car it)) (cdr it)) t) (setq letter (+ 1 letter))))) (t (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." (save-excursion (save-match-data (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)