/ emacs.d / rails / rails.el
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)