rails.el
1 ;;; rails.el --- minor mode for editing RubyOnRails code 2 3 ;; Copyright (C) 2006 Dmitry Galinsky <dima dot exe at gmail dot com> 4 5 ;; Authors: Dmitry Galinsky <dima dot exe at gmail dot com>, 6 ;; Rezikov Peter <crazypit13 (at) gmail.com> 7 8 ;; Keywords: ruby rails languages oop 9 ;; $URL: svn://rubyforge.org/var/svn/emacs-rails/trunk/rails.el $ 10 ;; $Id: rails.el 193 2007-05-05 18:37:00Z dimaexe $ 11 12 ;;; License 13 14 ;; This program is free software; you can redistribute it and/or 15 ;; modify it under the terms of the GNU General Public License 16 ;; as published by the Free Software Foundation; either version 2 17 ;; of the License, or (at your option) any later version. 18 19 ;; This program is distributed in the hope that it will be useful, 20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 ;; GNU General Public License for more details. 23 24 ;; You should have received a copy of the GNU General Public License 25 ;; along with this program; if not, write to the Free Software 26 ;; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 27 28 ;;; Code: 29 30 (unless (<= 22 emacs-major-version) 31 (error 32 (format "emacs-rails require CVS version of Emacs (future Emacs 22), and not be running on your Emacs %s.%s" 33 emacs-major-version 34 emacs-minor-version))) 35 36 (eval-when-compile 37 (require 'speedbar) 38 (require 'inf-ruby) 39 (require 'ruby-mode) 40 (require 'ruby-electric)) 41 42 (require 'sql) 43 (require 'ansi-color) 44 (require 'etags) 45 (require 'find-recursive) 46 47 (require 'untabify-file) 48 (require 'predictive-prog-mode) 49 50 (require 'inflections) 51 52 (require 'rails-compat) 53 (require 'rails-project) 54 55 (require 'rails-core) 56 (require 'rails-ruby) 57 (require 'rails-lib) 58 59 (require 'rails-cmd-proxy) 60 (require 'rails-navigation) 61 (require 'rails-find) 62 (require 'rails-scripts) 63 (require 'rails-rake) 64 (require 'rails-test) 65 (require 'rails-ws) 66 (require 'rails-log) 67 (require 'rails-ui) 68 (require 'rails-model-layout) 69 (require 'rails-controller-layout) 70 (require 'rails-features) 71 72 73 ;;;;;;;;;; Variable definition ;;;;;;;;;; 74 75 (defgroup rails nil 76 "Edit Rails projects with Emacs." 77 :group 'programming 78 :prefix "rails-") 79 80 (defcustom rails-api-root nil 81 "*Root of Rails API html documentation. Must be a local directory." 82 :group 'rails 83 :type 'string) 84 85 (defcustom rails-use-alternative-browse-url nil 86 "Indicates an alternative way of loading URLs on Windows. 87 Try using the normal method before. If URLs invoked by the 88 program don't end up in the right place, set this option to 89 true." 90 :group 'rails 91 :type 'boolean) 92 93 (defcustom rails-browse-api-with-w3m nil 94 "Indicates that the user wants to browse the Rails API using 95 Emacs w3m browser." 96 :group 'rails 97 :type 'boolean) 98 99 (defcustom rails-tags-command "ctags -e -a --Ruby-kinds=-f -o %s -R %s" 100 "Command used to generate TAGS in Rails root" 101 :group 'rails 102 :type 'string) 103 104 (defcustom rails-ri-command "ri" 105 "Command used to invoke the ri utility." 106 :group 'rails 107 :type 'string) 108 109 (defcustom rails-always-use-text-menus nil 110 "Force the use of text menus by default." 111 :group 'rails 112 :type 'boolean) 113 114 (defcustom rails-ask-when-reload-tags nil 115 "Indicates whether the user should confirm reload a TAGS table or not." 116 :group 'rails 117 :type 'boolean) 118 119 (defcustom rails-chm-file nil 120 "Path to CHM documentation file on Windows, or nil." 121 :group 'rails 122 :type 'string) 123 124 (defcustom rails-ruby-command "ruby" 125 "Ruby preferred command line invocation." 126 :group 'rails 127 :type 'string) 128 129 (defcustom rails-layout-template 130 "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" 131 \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"> 132 <html xmlns=\"http://www.w3.org/1999/xhtml\" 133 xml:lang=\"en\" lang=\"en\"> 134 <head> 135 <title></title> 136 <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /> 137 <%= stylesheet_link_tag \"default\" %> 138 </head> 139 140 <body> 141 <%= yield %> 142 </body> 143 </html>" 144 "Default html template for new rails layout" 145 :group 'rails 146 :type 'string) 147 148 (defvar rails-version "0.5.99.1") 149 (defvar rails-templates-list '("erb" "rhtml" "rxml" "rjs" "haml" "liquid")) 150 (defvar rails-use-another-define-key nil) 151 (defvar rails-primary-switch-func nil) 152 (defvar rails-secondary-switch-func nil) 153 154 (defvar rails-directory<-->types 155 '((:controller "app/controllers/") 156 (:layout "app/layouts/") 157 (:view "app/views/") 158 (:observer "app/models/" (lambda (file) (rails-core:observer-p file))) 159 (:mailer "app/models/" (lambda (file) (rails-core:mailer-p file))) 160 (:model "app/models/" (lambda (file) (and (not (rails-core:mailer-p file)) 161 (not (rails-core:observer-p file))))) 162 (:helper "app/helpers/") 163 (:plugin "vendor/plugins/") 164 (:unit-test "test/unit/") 165 (:functional-test "test/functional/") 166 (:fixture "test/fixtures/") 167 (:migration "db/migrate")) 168 "Rails file types -- rails directories map") 169 170 (defvar rails-enviroments '("development" "production" "test")) 171 (defvar rails-default-environment (first rails-enviroments)) 172 173 (defvar rails-adapters-alist 174 '(("mysql" . sql-mysql) 175 ("postgresql" . sql-postgres) 176 ("sqlite3" . sql-sqlite)) 177 "Sets emacs sql function for rails adapter names.") 178 179 (defvar rails-tags-dirs '("app" "lib" "test" "db") 180 "List of directories from RAILS_ROOT where ctags works.") 181 182 (defun rails-use-text-menu () 183 "If t use text menu, popup menu otherwise" 184 (or (null window-system) rails-always-use-text-menus)) 185 186 ;;;;;;;; hack ;;;; 187 (defun rails-svn-status-into-root () 188 (interactive) 189 (rails-project:with-root (root) 190 (svn-status root))) 191 192 ;; helper functions/macros 193 (defun rails-search-doc (&optional item) 194 (interactive) 195 (setq item (if item item (thing-at-point 'sexp))) 196 (unless item 197 (setq item (read-string "Search symbol: "))) 198 (if item 199 (if (and rails-chm-file 200 (file-exists-p rails-chm-file)) 201 (start-process "keyhh" "*keyhh*" "keyhh.exe" "-#klink" 202 (format "'%s'" item) rails-chm-file) 203 (let ((buf (buffer-name))) 204 (unless (string= buf "*ri*") 205 (switch-to-buffer-other-window "*ri*")) 206 (setq buffer-read-only nil) 207 (kill-region (point-min) (point-max)) 208 (message (concat "Please wait...")) 209 (call-process rails-ri-command nil "*ri*" t item) 210 (local-set-key [return] 'rails-search-doc) 211 (ansi-color-apply-on-region (point-min) (point-max)) 212 (setq buffer-read-only t) 213 (goto-char (point-min)))))) 214 215 (defun rails-create-tags() 216 "Create tags file" 217 (interactive) 218 (rails-project:in-root 219 (message "Creating TAGS, please wait...") 220 (let ((tags-file-name (rails-core:file "TAGS"))) 221 (shell-command 222 (format rails-tags-command tags-file-name 223 (strings-join " " (mapcar #'rails-core:file rails-tags-dirs)))) 224 (flet ((yes-or-no-p (p) (if rails-ask-when-reload-tags 225 (y-or-n-p p) 226 t))) 227 (visit-tags-table tags-file-name))))) 228 229 (defun rails-apply-for-buffer-type () 230 (let* ((type (rails-core:buffer-type)) 231 (name (substring (symbol-name type) 1)) 232 (minor-mode-name (format "rails-%s-minor-mode" name)) 233 (minor-mode-abbrev (concat minor-mode-name "-abbrev-table"))) 234 (when (require (intern minor-mode-name) nil t) ;; load new style minor mode rails-*-minor-mode 235 (when (fboundp (intern minor-mode-name)) 236 (apply (intern minor-mode-name) (list t)) 237 (when (boundp (intern minor-mode-abbrev)) 238 (merge-abbrev-tables 239 (symbol-value (intern minor-mode-abbrev)) 240 local-abbrev-table)))))) 241 242 ;;;;;;;;;; Database integration ;;;;;;;;;; 243 244 (defstruct rails-db-conf adapter host database username password) 245 246 (defun rails-db-parameters (env) 247 "Return database parameters for enviroment ENV" 248 (with-temp-buffer 249 (shell-command 250 (format "ruby -r yaml -r erb -e 'YAML.load(ERB.new(ARGF.read).result)[\"%s\"].to_yaml.display' %s" 251 env 252 (rails-core:file "config/database.yml")) 253 (current-buffer)) 254 (let ((answer 255 (make-rails-db-conf 256 :adapter (yml-value "adapter") 257 :host (yml-value "host") 258 :database (yml-value "database") 259 :username (yml-value "username") 260 :password (yml-value "password")))) 261 answer))) 262 263 (defun rails-database-emacs-func (adapter) 264 "Return the Emacs function for ADAPTER that, when run, will 265 +invoke the appropriate database server console." 266 (cdr (assoc adapter rails-adapters-alist))) 267 268 (defun rails-read-enviroment-name (&optional default) 269 "Read Rails enviroment with auto-completion." 270 (completing-read "Environment name: " (list->alist rails-enviroments) nil nil default)) 271 272 (defun* rails-run-sql (&optional env) 273 "Run a SQL process for the current Rails project." 274 (interactive (list (rails-read-enviroment-name "development"))) 275 (rails-project:with-root (root) 276 (cd root) 277 (if (bufferp (sql-find-sqli-buffer)) 278 (switch-to-buffer-other-window (sql-find-sqli-buffer)) 279 (let ((conf (rails-db-parameters env))) 280 (let ((sql-database (rails-db-conf-database conf)) 281 (default-process-coding-system '(utf-8 . utf-8)) 282 (sql-server (rails-db-conf-host conf)) 283 (sql-user (rails-db-conf-username conf)) 284 (sql-password (rails-db-conf-password conf))) 285 ;; Reload localy sql-get-login to avoid asking of confirmation of DB login parameters 286 (flet ((sql-get-login (&rest pars) () t)) 287 (funcall (rails-database-emacs-func (rails-db-conf-adapter conf))))))))) 288 289 (defun rails-has-api-root () 290 "Test whether `rails-api-root' is configured or not, and offer to configure 291 it in case it's still empty for the project." 292 (rails-project:with-root 293 (root) 294 (unless (or (file-exists-p (rails-core:file "doc/api/index.html")) 295 (not (yes-or-no-p (concat "This project has no API documentation. " 296 "Would you like to configure it now? ")))) 297 (let (clobber-gems) 298 (message "This may take a while. Please wait...") 299 (unless (file-exists-p (rails-core:file "vendor/rails")) 300 (setq clobber-gems t) 301 (message "Freezing gems...") 302 (shell-command-to-string "rake rails:freeze:gems")) 303 ;; Hack to allow generation of the documentation for Rails 1.0 and 1.1 304 ;; See http://dev.rubyonrails.org/ticket/4459 305 (unless (file-exists-p (rails-core:file "vendor/rails/activesupport/README")) 306 (write-string-to-file (rails-core:file "vendor/rails/activesupport/README") 307 "Placeholder")) 308 (message "Generating documentation...") 309 (shell-command-to-string "rake doc:rails") 310 (if clobber-gems 311 (progn 312 (message "Unfreezing gems...") 313 (shell-command-to-string "rake rails:unfreeze"))) 314 (message "Done."))) 315 (if (file-exists-p (rails-core:file "doc/api/index.html")) 316 (setq rails-api-root (rails-core:file "doc/api"))))) 317 318 (defun rails-browse-api () 319 "Browse Rails API on RAILS-API-ROOT." 320 (interactive) 321 (if (rails-has-api-root) 322 (rails-browse-api-url (concat rails-api-root "/index.html")) 323 (message "Please configure variable rails-api-root."))) 324 325 (defun rails-get-api-entries (name file sexp get-file-func) 326 "Return all API entries named NAME in file FILE using SEXP to 327 find matches, and GET-FILE-FUNC to process the matches found." 328 (if (file-exists-p (concat rails-api-root "/" file)) 329 (save-current-buffer 330 (save-match-data 331 (find-file (concat rails-api-root "/" file)) 332 (let* ((result 333 (loop for line in (split-string (buffer-string) "\n") 334 when (string-match (format sexp (regexp-quote name)) line) 335 collect (cons (match-string-no-properties 2 line) 336 (match-string-no-properties 1 line))))) 337 (kill-buffer (current-buffer)) 338 (when-bind (api-file (funcall get-file-func result)) 339 (rails-browse-api-url (concat "file://" rails-api-root "/" api-file)))))) 340 (message "There are no API docs."))) 341 342 (defun rails-browse-api-class (class) 343 "Browse the Rails API documentation for CLASS." 344 (rails-get-api-entries 345 class "fr_class_index.html" "<a href=\"\\(.*\\)\">%s<" 346 (lambda (entries) 347 (cond ((= 0 (length entries)) (progn (message "No API Rails doc for class %s." class) nil)) 348 ((= 1 (length entries)) (cdar entries)))))) 349 350 (defun rails-browse-api-method (method) 351 "Browse the Rails API documentation for METHOD." 352 (rails-get-api-entries 353 method "fr_method_index.html" "<a href=\"\\(.*\\)\">%s[ ]+(\\(.*\\))" 354 (lambda (entries) 355 (cond ((= 0 (length entries)) (progn (message "No API Rails doc for %s" method) nil)) 356 ((= 1 (length entries)) (cdar entries)) 357 (t (cdr (assoc (completing-read (format "Method %s from what class? " method) entries) 358 entries))))))) 359 360 (defun rails-browse-api-at-point () 361 "Open the Rails API documentation on the class or method at the current point. 362 The variable `rails-api-root' must be pointing to a local path 363 either in your project or elsewhere in the filesystem. The 364 function will also offer to build the documentation locally if 365 necessary." 366 (interactive) 367 (if (rails-has-api-root) 368 (let ((current-symbol (prog2 369 (modify-syntax-entry ?: "w") 370 (thing-at-point 'sexp) 371 (modify-syntax-entry ?: ".")))) 372 (if current-symbol 373 (if (capital-word-p current-symbol) 374 (rails-browse-api-class current-symbol) 375 (rails-browse-api-method current-symbol)))) 376 (message "Please configure \"rails-api-root\"."))) 377 378 ;;; Rails minor mode 379 380 (define-minor-mode rails-minor-mode 381 "RubyOnRails" 382 nil 383 " Rails" 384 rails-minor-mode-map 385 (abbrev-mode -1) 386 (make-local-variable 'tags-file-name) 387 (make-local-variable 'rails-primary-switch-func) 388 (make-local-variable 'rails-secondary-switch-func) 389 (rails-features:install)) 390 391 ;; hooks 392 393 (add-hook 'ruby-mode-hook 394 (lambda() 395 (require 'rails-ruby) 396 (require 'ruby-electric) 397 (ruby-electric-mode t) 398 (imenu-add-to-menubar "IMENU") 399 (modify-syntax-entry ?! "w" (syntax-table)) 400 (modify-syntax-entry ?: "w" (syntax-table)) 401 (modify-syntax-entry ?_ "w" (syntax-table)) 402 (local-set-key (kbd "C-.") 'complete-tag) 403 (local-set-key (if rails-use-another-define-key 404 (kbd "TAB") (kbd "<tab>")) 405 'indent-or-complete) 406 (local-set-key (rails-key "f") '(lambda() 407 (interactive) 408 (mouse-major-mode-menu (rails-core:menu-position)))) 409 (local-set-key (kbd "C-:") 'ruby-toggle-string<>simbol) 410 (local-set-key (if rails-use-another-define-key 411 (kbd "RET") (kbd "<return>")) 412 'ruby-newline-and-indent))) 413 414 (add-hook 'speedbar-mode-hook 415 (lambda() 416 (speedbar-add-supported-extension "\\.rb"))) 417 418 (add-hook 'find-file-hooks 419 (lambda() 420 (rails-project:with-root 421 (root) 422 (progn 423 (local-set-key (if rails-use-another-define-key 424 (kbd "TAB") (kbd "<tab>")) 425 'indent-or-complete) 426 (rails-minor-mode t) 427 (rails-apply-for-buffer-type))))) 428 429 ;; Run rails-minor-mode in dired 430 431 (add-hook 'dired-mode-hook 432 (lambda () 433 (if (rails-project:root) 434 (rails-minor-mode t)))) 435 436 437 (autoload 'haml-mode "haml-mode" "" t) 438 439 (setq auto-mode-alist (cons '("\\.rb$" . ruby-mode) auto-mode-alist)) 440 (setq auto-mode-alist (cons '("\\.rake$" . ruby-mode) auto-mode-alist)) 441 (setq auto-mode-alist (cons '("Rakefile$" . ruby-mode) auto-mode-alist)) 442 (setq auto-mode-alist (cons '("\\.haml$" . haml-mode) auto-mode-alist)) 443 (setq auto-mode-alist (cons '("\\.rjs$" . ruby-mode) auto-mode-alist)) 444 (setq auto-mode-alist (cons '("\\.rxml$" . ruby-mode) auto-mode-alist)) 445 (setq auto-mode-alist (cons '("\\.rhtml$" . html-mode) auto-mode-alist)) 446 447 (modify-coding-system-alist 'file "\\.rb$" 'utf-8) 448 (modify-coding-system-alist 'file "\\.rake$" 'utf-8) 449 (modify-coding-system-alist 'file "Rakefile$" 'utf-8) 450 (modify-coding-system-alist 'file (rails-core:regex-for-match-view) 'utf-8) 451 452 (provide 'rails)