1
;;; rails-core.el --- core helper functions and macros for emacs-rails
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$
10
;; $Id$
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
(eval-when-compile
29
  (require 'rails-lib))
30
31
(defcustom rails-core:class-dirs
32
  '("app/controllers"
33
    "app/views"
34
    "app/models"
35
    "app/helpers"
36
    "test/unit"
37
    "test/functional"
38
    "test/fixtures"
39
    "spec/controllers"
40
    "spec/requests"
41
    "spec/fixtures"
42
    "spec/lib"
43
    "spec/models"
44
    "lib")
45
  "Directories with Rails classes"
46
  :group 'rails
47
  :type '(repeat string))
48
49
(defun rails-core:class-by-file (filename)
50
  "Return the class associated with FILENAME.
51
   <rails-root>/(app/models|app/controllers|app/helpers|test/unit|test/functional|lib|spec/controllers|spec/lib|spec/models)/foo/bar_baz
52
                --> Foo::BarBaz"
53
  (let* ((case-fold-search nil)
54
         (path (replace-regexp-in-string
55
                (format
56
                 "\\(.*\\(%s\\)/\\)?\\([^\.]+\\)\\(.*\\)?"
57
                 (strings-join "\\|" rails-core:class-dirs)) "\\3" filename))
58
         (path (replace-regexp-in-string "/" "  " path))
59
         (path (replace-regexp-in-string "_" " " path)))
60
    (replace-regexp-in-string
61
     " " ""
62
     (replace-regexp-in-string
63
      "  " "::"
64
      (if (string-match "^ *\\([0-9]+ *\\)?[A-Z]" path)
65
          path
66
        (capitalize path))))))
67
68
(defun rails-core:file-by-class (classname &optional do-not-append-ext)
69
  "Return the filename associated with CLASSNAME.
70
If the optional parameter DO-NOT-APPEND-EXT is set this function
71
will not append \".rb\" to result."
72
  (concat (decamelize (replace-regexp-in-string "::" "/" classname))
73
          (unless do-not-append-ext ".rb")))
74
75
;;;;;;;;;; Files ;;;;;;;;;;
76
77
(defun rails-core:file (file-name)
78
  "Return the full path for FILE-NAME in a Rails directory."
79
  (when file-name
80
    (if (file-name-absolute-p file-name)
81
        file-name
82
      (rails-project:with-root
83
       (root)
84
       (concat root file-name)))))
85
86
(defun rails-core:quoted-file (file-name)
87
  "Return the quoted full path for FILE-NAME in a Rails directory."
88
  (concat "\"" (rails-core:file file-name) "\""))
89
90
(defun rails-core:find-file (file-name)
91
  "Open the file named FILE_NAME in a Rails directory."
92
  (when-bind (file (rails-core:file file-name))
93
       (find-file file)))
94
95
(defun rails-core:find-file-if-exist (file-name)
96
  "Open the file named FILE-NAME in a Rails directory only if the file exists."
97
  (let ((file-name (rails-core:file file-name)))
98
    (when (and file-name (file-exists-p file-name))
99
      (find-file file-name))))
100
101
(defun rails-core:find-or-ask-to-create (question file)
102
  "Open the file named FILE in a Rails directory if it exists. If
103
it does not exist, ask to create it using QUESTION as a prompt."
104
  (find-or-ask-to-create question (rails-core:file file)))
105
106
(defun rails-core:strip-namespace (class-name)
107
  "Strip namespace of CLASS-NAME, eg Foo::Bar -> Bar."
108
  (let ((name-list (split-string class-name "::")))
109
    (car (last name-list))))
110
111
;; Funtions, that retrun Rails objects full pathes
112
113
(defun rails-core:model-file (model-name)
114
  "Return the model file from the model name."
115
  (when model-name
116
    (let* ((stripped-model-file
117
            (rails-core:file-by-class
118
             (rails-core:strip-namespace model-name)))
119
           (model-file
120
            (rails-core:file-by-class model-name)))
121
      (cond
122
       ((file-exists-p
123
         (rails-core:file (concat "app/models/" model-file)))
124
        (concat "app/models/" model-file))
125
       ((file-exists-p
126
         (rails-core:file (concat "app/models/" stripped-model-file)))
127
        (concat "app/models/" stripped-model-file))
128
       (t
129
        (concat "app/models/" model-file))))))
130
131
(defun rails-core:model-exist-p (model-name)
132
  "Return t if model MODEL-NAME exist."
133
  (let ((model-file (rails-core:model-file model-name)))
134
    (when model-file
135
      (and (file-exists-p (rails-core:file model-file))
136
           (not (rails-core:observer-p model-name))
137
           (not (rails-core:mailer-p model-name))))))
138
139
(defun rails-core:controller-file (controller-name)
140
  "Return the path to the controller CONTROLLER-NAME."
141
  (when controller-name
142
    (let* ((basename (rails-core:file-by-class (rails-core:short-controller-name controller-name) t))
143
	   (exact (concat "app/controllers/" basename ".rb")))
144
      (if (file-exists-p (rails-core:file exact))
145
	exact
146
	(concat "app/controllers/" basename "_controller.rb")))))
147
148
(defun rails-core:controller-exist-p (controller-name)
149
  "Return t if controller CONTROLLER-NAME exist."
150
  (when controller-name
151
    (file-exists-p
152
     (rails-core:file
153
      (rails-core:controller-file controller-name)))))
154
155
(defun rails-core:controller-file-by-model (model)
156
  (when model
157
    (let* ((controller (pluralize-string model)))
158
           ;(controller (when controller (capitalize controller))))
159
      (setq controller
160
            (cond
161
             ((rails-core:controller-exist-p controller) controller) ;; pluralized
162
             ((rails-core:controller-exist-p model) model) ;; singularized
163
             (t (let ((controllers (rails-core:controllers t)))
164
                  (cond
165
                   ;; with namespace
166
                   ((find
167
                     (list controller model)
168
                     controllers
169
                     :test #'(lambda(x y)
170
                               (or
171
                                (string= (car x) (rails-core:strip-namespace y))
172
                                (string= (cadr x) (rails-core:strip-namespace y)))))))))))
173
      (when controller
174
        (rails-core:controller-file controller)))))
175
176
(defun rails-core:observer-file (observer-name)
177
  "Return the path to the observer OBSERVER-NAME."
178
  (when observer-name
179
    (rails-core:model-file (concat observer-name "Observer"))))
180
181
(defun rails-core:mailer-file (mailer)
182
  (when (and mailer
183
             (rails-core:mailer-p mailer))
184
    (rails-core:model-file mailer)))
185
186
(defun rails-core:mailer-exist-p (mailer)
187
  (when mailer
188
    (file-exists-p (rails-core:file (rails-core:mailer-file mailer)))))
189
190
(defun rails-core:migration-file (migration-name)
191
  "Return the model file from the MIGRATION-NAME."
192
  (when migration-name
193
    (let ((dir "db/migrate/")
194
          (name (replace-regexp-in-string
195
                 " " "_"
196
                 (rails-core:file-by-class migration-name))))
197
      (when (string-match "^[^0-9]+[^_]" name) ; try search when the name without migration number
198
        (let ((files (directory-files (rails-core:file dir)
199
                                      nil
200
                                      (concat "[0-9]+_" name "$"))))
201
          (setq name (if files
202
                         (car files)
203
                       nil))))
204
      (when name
205
        (concat dir name)))))
206
207
(defun rails-core:migration-file-by-model (model)
208
  (when model
209
    (rails-core:migration-file
210
     (concat "Create" (rails-core:class-by-file (pluralize-string model))))))
211
212
(defun rails-core:model-by-migration-filename (migration-filename)
213
  (when migration-filename
214
    (let ((model-name (singularize-string
215
                       (string=~ "[0-9]+_create_\\(\\w+\\)\.rb" (buffer-name) $1))))
216
      (when (and model-name
217
                 (rails-core:model-exist-p model-name))
218
        model-name))))
219
220
(defun rails-core:configuration-file (file)
221
  "Return the path to the configuration FILE."
222
  (when file
223
    (concat "config/" file)))
224
225
(defun rails-core:plugin-file (plugin file)
226
  "Return the path to the FILE in Rails PLUGIN."
227
  (concat "vendor/plugins/" plugin "/" file))
228
229
(defun rails-core:layout-file (layout)
230
  "Return the path to the layout file named LAYOUT."
231
  (let ((its rails-templates-list)
232
        filename)
233
    (while (and (car its)
234
                (not filename))
235
      (when (file-exists-p (format "%sapp/views/layouts/%s.%s" (rails-project:root) layout (car its)))
236
        (setq filename (format "app/views/layouts/%s.%s" layout (car its))))
237
      (setq its (cdr its)))
238
    filename))
239
240
(defun rails-core:js-file (js)
241
  "Return the path to the JavaScript file named JS."
242
  (concat "public/javascripts/" js ".js"))
243
244
(defun rails-core:partial-name (name)
245
  "Return the file name of partial NAME."
246
  (if (string-match "/" name)
247
      (concat "app/views/"
248
        (replace-regexp-in-string "\\([^/]*\\)$" "_\\1.html.erb" name))
249
    (concat (rails-core:views-dir (rails-core:current-controller))
250
      "_" name ".html.erb")))
251
252
(defun rails-core:view-name (name)
253
  "Return the file name of view NAME."
254
  (concat (rails-core:views-dir (rails-core:current-controller))
255
          name ".html.erb")) ;; BUG: will fix it
256
257
(defun rails-core:helper-file (controller)
258
  "Return the helper file name for the controller named
259
CONTROLLER."
260
  (if (string= "Test/TestHelper" controller)
261
      (rails-core:file (rails-core:file-by-class "Test/TestHelper"))
262
    (when controller
263
      (format "app/helpers/%s_helper.rb"
264
              (replace-regexp-in-string "_controller" ""
265
                                        (rails-core:file-by-class controller t))))))
266
267
(defun rails-core:helper-test-file (controller)
268
  (when controller
269
    (format "test/unit/helpers/%s_helper_test.rb" (rails-core:file-by-class controller t))))
270
(assert (string= "test/unit/helpers/foo/bar_quux_helper_test.rb" (rails-core:helper-test-file "Foo::BarQuux")))
271
272
(defun rails-core:functional-test-file (controller)
273
  "Return the functional test file name for the controller named
274
CONTROLLER."
275
  (when controller
276
    (format "test/functional/%s_test.rb"
277
            (rails-core:file-by-class (rails-core:long-controller-name controller) t))))
278
279
(defun rails-core:unit-test-file (model)
280
  "Return the unit test file name for the model named MODEL."
281
  (when model
282
    (format "test/unit/%s_test.rb" (rails-core:file-by-class model t))))
283
284
(defun rails-core:unit-test-exist-p (model)
285
  "Return the unit test file name for the model named MODEL."
286
  (let ((test (rails-core:unit-test-file model)))
287
    (when test
288
      (file-exists-p (rails-core:file test)))))
289
290
(defun rails-core:fixture-file (model)
291
  "Return the fixtures file name for the model named MODEL."
292
  (when model
293
    (format "test/fixtures/%s.yml" (pluralize-string (rails-core:file-by-class model t)))))
294
295
(defun rails-core:fixture-exist-p (model)
296
  (when model
297
    (file-exists-p
298
     (rails-core:file (rails-core:fixture-file model)))))
299
300
(defun rails-core:views-dir (controller)
301
  "Return the view directory name for the controller named CONTROLLER."
302
  (format "app/views/%s/" (replace-regexp-in-string "_controller" "" (rails-core:file-by-class controller t))))
303
304
(defun rails-core:stylesheet-name (name)
305
  "Return the file name of the stylesheet named NAME."
306
  (concat "public/stylesheets/" name ".css"))
307
308
(defun rails-core:controller-name (controller-file)
309
  "Return the class name of the controller named CONTROLLER.
310
   Bar in Foo dir -> Foo::Bar"
311
  (rails-core:class-by-file
312
   (if (eq (elt controller-file 0) 47) ;;; 47 == '/'
313
       (subseq controller-file 1)
314
     (let ((current-controller (rails-core:current-controller)))
315
       (if (string-match ":" current-controller)
316
     (concat (replace-regexp-in-string "[^:]*$" "" current-controller)
317
       controller-file)
318
   controller-file)))))
319
320
(defun rails-core:short-controller-name (controller)
321
  "Convert FooController -> Foo."
322
  (remove-postfix  controller "Controller" ))
323
324
(defun rails-core:long-controller-name (controller)
325
  "Convert Foo/FooController -> FooController."
326
  (if  (string-match "Controller$" controller)
327
      controller
328
    (concat controller "Controller")))
329
330
(defun rails-core:rspec-controller-file (controller)
331
  "Return the controller spec file name for the controller named
332
CONTROLLER."
333
  (when controller
334
    (let ((existing (find-if #'file-exists-p (mapcar (lambda (pattern)
335
                                                       (rails-core:file (format pattern
336
                                                                                (rails-core:file-by-class controller t))))
337
                                                     '("spec/requests/%s_spec.rb"
338
                                                       "spec/controllers/%s_controller_spec.rb")))))
339
      (if existing existing (rails-core:file (format "spec/controllers/%s_controller_spec.rb" (rails-core:file-by-class controller t)))))))
340
341
(defun rails-core:lib-file (lib-name)
342
  "Return the model file from the lib name."
343
  (when lib-name
344
    (concat "lib/" (rails-core:file-by-class lib-name))))
345
346
(defun rails-core:rspec-lib-file (lib)
347
  "Return the lib spec file name for the lib named LIB."
348
  (when lib
349
    (format "spec/lib/%s_spec.rb" (rails-core:file-by-class lib t))))
350
351
(defun rails-core:rspec-model-file (model)
352
  "Return the model spec file name for the model named MODEL."
353
  (when model
354
    (format "spec/models/%s_spec.rb" (rails-core:file-by-class model t))))
355
356
(defun rails-core:rspec-fixture-file (model)
357
  "Return the rspec fixtures file name for the model named MODEL."
358
  (when model
359
    (format "spec/fixtures/%s.yml" (pluralize-string (rails-core:file-by-class model t)))))
360
361
(defun rails-core:rspec-lib-exist-p (lib)
362
  "Return the lib spec file name for the model named MODEL."
363
  (let ((spec (rails-core:rspec-lib-file lib)))
364
    (when spec
365
      (file-exists-p (rails-core:file spec)))))
366
367
(defun rails-core:rspec-model-exist-p (model)
368
  "Return the model spec file name for the model named MODEL."
369
  (let ((spec (rails-core:rspec-model-file model)))
370
    (when spec
371
      (file-exists-p (rails-core:file spec)))))
372
373
(defun rails-core:rspec-fixture-exist-p (model)
374
  (when model
375
    (file-exists-p
376
     (rails-core:file (rails-core:rspec-fixture-file model)))))
377
378
;;;;;;;;;; Functions that return collection of Rails objects  ;;;;;;;;;;
379
(defun rails-core:observer-p (name)
380
  (when name
381
    (if (string-match "\\(Observer\\|_observer\\)\\(\\.rb\\)?$" name)
382
        t nil)))
383
384
(defun rails-core:mailer-p (name)
385
  (when name
386
    (if (string-match "\\(Mailer\\|Notifier\\|_mailer\\|_notifier\\)\\(\\.rb\\)?$" name)
387
        t nil)))
388
389
390
(defun rails-core:controllers (&optional cut-contoller-suffix)
391
  "Return a list of Rails controllers. Remove the '_controller'
392
suffix if CUT-CONTOLLER-SUFFIX is non nil."
393
  (mapcar
394
   #'(lambda (controller)
395
       (rails-core:class-by-file
396
        (if cut-contoller-suffix
397
            (replace-regexp-in-string "_controller\\." "." controller)
398
          controller)))
399
   (delete-if-not
400
    #'(lambda (controller)
401
        (string-match "\\(application\\|[a-z0-9_]+_controller\\)\\.rb$"
402
                      controller))
403
    (directory-files-recursive (rails-core:file "app/controllers/") nil "\\.rb$"))))
404
405
(defun rails-core:functional-tests ()
406
  "Return a list of Rails functional tests."
407
  (mapcar
408
   #'(lambda(it)
409
       (remove-postfix (rails-core:class-by-file it)
410
                       "ControllerTest"))
411
   (directory-files-recursive (rails-core:file "test/functional/") nil "\\.rb$")))
412
413
(defun rails-core:models ()
414
  "Return a list of Rails models."
415
  (mapcar
416
   #'rails-core:class-by-file
417
   (delete-if
418
    #'(lambda (file) (or (rails-core:observer-p file)
419
                         (rails-core:mailer-p file)))
420
    (directory-files-recursive (rails-core:file "app/models/") nil "\\.rb$"))))
421
422
(defun rails-core:unit-tests ()
423
  "Return a list of Rails functional tests."
424
  (mapcar
425
   #'(lambda(it)
426
       (remove-postfix (rails-core:class-by-file it)
427
                       "Test"))
428
   (directory-files-recursive (rails-core:file "test/unit/") nil "\\.rb$")))
429
430
(defun rails-core:observers ()
431
  "Return a list of Rails observers."
432
  (mapcar
433
   #'(lambda (observer) (replace-regexp-in-string "Observer$" "" observer))
434
   (mapcar
435
    #'rails-core:class-by-file
436
    (directory-files-recursive (rails-core:file "app/models/") nil "\\(_observer\\)\\.rb$"))))
437
438
(defun rails-core:mailers ()
439
  "Return a list of Rails mailers."
440
  (mapcar
441
   #'rails-core:class-by-file
442
   (directory-files-recursive (rails-core:file "app/models/") nil "\\(_mailer\\|_notifier\\)\\.rb$")))
443
444
(defun rails-core:helpers ()
445
  "Return a list of Rails helpers."
446
  (append
447
   (mapcar
448
    #'(lambda (helper) (replace-regexp-in-string "Helper$" "" helper))
449
    (mapcar
450
     #'rails-core:class-by-file
451
     (directory-files-recursive (rails-core:file "app/helpers/") nil "_helper\\.rb$")))
452
   (list "Test/TestHelper")))
453
454
(defun rails-core:migrations (&optional strip-numbers)
455
  "Return a list of Rails migrations."
456
  (let (migrations)
457
    (setq
458
     migrations
459
     (reverse
460
      (mapcar
461
       #'(lambda (migration)
462
           (replace-regexp-in-string "^\\([0-9]+\\)" "\\1 " migration))
463
       (mapcar
464
        #'rails-core:class-by-file
465
        (directory-files-recursive (rails-core:file "db/migrate") nil "^[0-9]+_.*\\.rb$")))))
466
    (if strip-numbers
467
        (mapcar #'(lambda(i) (car (last (split-string i " "))))
468
                migrations)
469
      migrations)))
470
471
(defun rails-core:migration-versions (&optional with-zero)
472
  "Return a list of migtaion versions as the list of strings. If
473
second argument WITH-ZERO is present, append the \"000\" version
474
of migration."
475
  (let ((ver (mapcar
476
              #'(lambda(it) (car (split-string it " ")))
477
              (rails-core:migrations))))
478
    (if with-zero
479
        (append ver '("000"))
480
      ver)))
481
482
(defun rails-core:plugins ()
483
  "Return a list of Rails plugins."
484
  (mapcar
485
   #'file-name-nondirectory
486
   (delete-if-not
487
    #'file-directory-p
488
    (directory-files (rails-core:file "vendor/plugins") t "^[^\\.]"))))
489
490
(defun rails-core:plugin-files (plugin)
491
  "Return a list of files in specific Rails plugin."
492
  (directory-files-recursive (rails-core:file (concat "vendor/plugins/" plugin)) nil  "^[^.]"))
493
494
(defun rails-core:layouts ()
495
  "Return a list of Rails layouts."
496
  (mapcar
497
   #'(lambda (l)
498
       (replace-regexp-in-string "\\.[^.]+$" "" l))
499
   (directory-files-recursive  (rails-core:file "app/views/layouts") nil (rails-core:regex-for-match-view))))
500
501
(defun rails-core:fixtures ()
502
  "Return a list of Rails fixtures."
503
  (mapcar
504
   #'(lambda (l)
505
       (replace-regexp-in-string "\\.[^.]+$" "" l))
506
   (directory-files-recursive (rails-core:file "test/fixtures/") nil "\\.yml$")))
507
508
(defun rails-core:configuration-files ()
509
  "Return a files of files from config folder."
510
  (directory-files-recursive (rails-core:file "config/")))
511
512
(defun rails-core:regex-for-match-view ()
513
  "Return a regex to match Rails view templates.
514
The file extensions used for views are defined in `rails-templates-list'."
515
  (format "\\.\\(%s\\)$" (strings-join "\\|" rails-templates-list)))
516
517
(defun rails-core:get-view-files (controller-class &optional action)
518
  "Retun a list containing the view file for CONTROLLER-CLASS#ACTION.
519
If the action is nil, return all views for the controller."
520
    (rails-project:with-root
521
     (root)
522
     (directory-files
523
      (rails-core:file
524
       (rails-core:views-dir
525
        (rails-core:short-controller-name controller-class))) t
526
        (if action
527
            (concat "^" action (rails-core:regex-for-match-view))
528
          (rails-core:regex-for-match-view)))))
529
530
(defun rails-core:extract-ancestors (classes)
531
  "Return the parent classes from a list of classes named CLASSES."
532
  (delete ""
533
   (uniq-list
534
   (mapcar (lambda (class)
535
       (replace-regexp-in-string
536
        "::[^:]*$" "::"
537
        (replace-regexp-in-string "^[^:]*$" "" class)))
538
     classes))))
539
540
(defun rails-core:models-ancestors ()
541
  "Return the parent classes of models."
542
  (rails-core:extract-ancestors (rails-core:models)))
543
544
(defun rails-core:controllers-ancestors ()
545
  "Return the parent classes of controllers."
546
  (rails-core:extract-ancestors (rails-core:controllers)))
547
548
(defun rails-core:rspec-controllers ()
549
  "Return a list of Rails controller specs."
550
  (mapcar
551
   #'(lambda(it)
552
       (remove-postfix (rails-core:class-by-file it)
553
                       "Spec"))
554
   (directory-files-recursive (rails-core:file "spec/controllers/") nil "\\.rb$")))
555
556
(defun rails-core:rspec-models ()
557
  "Return a list of Rails model specs."
558
  (mapcar
559
   #'(lambda(it)
560
       (remove-postfix (rails-core:class-by-file it)
561
                       "Spec"))
562
   (directory-files-recursive (rails-core:file "spec/models/") nil "\\.rb$")))
563
564
(defun rails-core:rspec-fixtures ()
565
  "Return a list of Rails RSpec fixtures."
566
  (mapcar
567
   #'(lambda (l)
568
       (replace-regexp-in-string "\\.[^.]+$" "" l))
569
   (directory-files-recursive (rails-core:file "spec/fixtures/") nil "\\.yml$")))
570
571
;;;;;;;;;; Getting Controllers/Model/Action from current buffer ;;;;;;;;;;
572
573
(defun rails-core:current-controller ()
574
  "Return the current Rails controller."
575
  (let* ((file-class (rails-core:class-by-file (buffer-file-name))))
576
    (unless (rails-core:mailer-p file-class)
577
      (case (rails-core:buffer-type)
578
        (:controller (rails-core:short-controller-name file-class))
579
        (:view (rails-core:class-by-file
580
                (directory-file-name (directory-of-file (buffer-file-name)))))
581
        (:helper (remove-postfix file-class "Helper"))
582
        (:functional-test (remove-postfix file-class "ControllerTest"))
583
        (:rspec-controller (remove-postfix file-class "Spec"))))))
584
585
(defun rails-core:current-model ()
586
  "Return the current Rails model."
587
  (let* ((file-class (rails-core:class-by-file (buffer-file-name))))
588
    (unless (rails-core:mailer-p file-class)
589
      (case (rails-core:buffer-type)
590
        (:migration (rails-core:model-by-migration-filename (buffer-name)))
591
        (:model file-class)
592
        (:unit-test (remove-postfix file-class "Test"))
593
        (:fixture (singularize-string file-class))
594
        (:rspec-fixture (singularize-string file-class))
595
        (:rspec-model (remove-postfix file-class "Spec"))))))
596
597
(defun rails-core:current-lib ()
598
  "Return the current lib."
599
  (let* ((file-class (rails-core:class-by-file (buffer-file-name))))
600
    (unless (rails-core:mailer-p file-class)
601
      (case (rails-core:buffer-type)
602
        (:lib file-class)
603
        (:rspec-lib (remove-postfix file-class "Spec"))))))
604
605
(defun rails-core:current-mailer ()
606
  "Return the current Rails Mailer, else return nil."
607
  (let* ((file-class (rails-core:class-by-file (buffer-file-name)))
608
         (test (remove-postfix file-class "Test"))
609
         (mailer-class (case (rails-core:buffer-type)
610
                         (:mailer    file-class)
611
                         (:unit-test test)
612
                         (:view      (rails-core:class-by-file
613
                                      (directory-file-name (directory-of-file (buffer-file-name))))))))
614
    (and (rails-core:mailer-p mailer-class) mailer-class)))
615
616
(defun rails-core:current-action ()
617
  "Return the current action in the current Rails controller."
618
  (case (rails-core:buffer-type)
619
    (:controller (rails-core:current-method-name))
620
    (:mailer (rails-core:current-method-name))
621
    (:view (string-match "/\\([a-z0-9_]+\\)\.[a-z]+$" (buffer-file-name))
622
           (match-string 1 (buffer-file-name)))))
623
624
(defun rails-core:current-helper ()
625
  "Return the current helper"
626
  (rails-core:current-controller))
627
628
(defun rails-core:current-plugin ()
629
  "Return the current plugin name."
630
  (let ((name (buffer-file-name)))
631
    (when (string-match "vendor\\/plugins\\/\\([^\\/]+\\)" name)
632
      (match-string 1 name))))
633
634
(defun rails-core:current-method-name ()
635
  (save-excursion
636
    (when (search-backward-regexp "^[ ]*def \\([a-z0-9_]+\\)" nil t)
637
      (match-string-no-properties 1))))
638
639
(defun rails-core:current-migration-version ()
640
  "Return the current migration version"
641
  (let ((name (buffer-file-name)))
642
    (when (string-match "db\\/migrate\\/\\([0-9]+\\)[a-z0-9_]+\.[a-z]+$" name)
643
      (match-string 1 name))))
644
645
(defun rails-core:grep-from-file (file regexp replacement)
646
  (and file
647
       (file-exists-p file)
648
       (with-temp-buffer
649
         (insert-file-contents file)
650
         (goto-char (point-min))
651
         (and (re-search-forward regexp nil t)
652
              (match-substitute-replacement replacement)))))
653
654
(defun rails-core:grep-from-runner (stmt regexp replacement)
655
  (and (rails-core:file "script/runner")
656
       (with-temp-buffer
657
         (let ((default-directory (rails-project:root)))
658
           (shell-command (concat rails-ruby-command " script/runner '" stmt "'")
659
                          (current-buffer)
660
                          nil)
661
           (goto-char (point-min))
662
           (and (re-search-forward regexp)
663
                (match-substitute-replacement replacement))))))
664
665
(defun rails-core:current-rails-version ()
666
  "Return the rails version of the current project"
667
  (let* ((version-rb-re (concat "MAJOR[[:space:]]+=[[:space:]]+\\([[:digit:]]+\\)"
668
                                "[[:space:]]+MINOR[[:space:]]+=[[:space:]]\\([[:digit:]]+\\)"
669
                                "[[:space:]]+TINY[[:space:]]+=[[:space:]]\\([[:digit:]]+\\)"))
670
         (fns
671
          (list
672
           (lambda ()
673
             (rails-core:grep-from-file (rails-core:file "Gemfile")
674
                                        "^gem[[:space:]]+\\(['\"]\\)rails\\1,[[:space:]]+\\1\\(.*?\\)\\1"
675
                                        "\\2"))
676
           (lambda ()
677
             (rails-core:grep-from-file (rails-core:file "config/environment.rb")
678
                                        "^RAILS_GEM_VERSION[[:space:]]+=[[:space:]]\\(['\"]\\)\\(.*?\\)\\1"
679
                                        "\\2"))
680
           (lambda ()
681
             (rails-core:grep-from-file (rails-core:file "vendor/rails/railties/lib/rails/version.rb")
682
                                        version-rb-re
683
                                        "\\1.\\2.\\3"))
684
           (lambda ()
685
             (rails-core:grep-from-file (rails-core:file "vendor/rails/railties/lib/rails_version.rb")
686
                                        version-rb-re
687
                                        "\\1.\\2.\\3"))
688
           (lambda ()
689
             (rails-core:grep-from-runner "puts Rails::VERSION::STRING"
690
                                          "^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+$"
691
                                          "\\&"))))
692
         (version nil))
693
    (while (and (not version) fns)
694
      (setq version (funcall (car fns))
695
            fns (cdr fns)))
696
    version))
697
698
(defun rails-core:current-rails-major-version ()
699
  "Return project major version of rails."
700
  (let ((version (rails-core:current-rails-version)))
701
    (when (string-match "^[0-9]+" version)
702
      (string-to-number (match-string 0 version)))))
703
704
;;;;;;;;;; Determination of buffer type ;;;;;;;;;;
705
706
(defun rails-core:buffer-file-match (regexp)
707
  "Match the current buffer file name to RAILS_ROOT + REGEXP."
708
  (when-bind (file (rails-core:file regexp))
709
             (string-match file
710
                           (buffer-file-name (current-buffer)))))
711
712
(defun rails-core:buffer-type ()
713
  "Return the type of the current Rails file or nil if the type
714
cannot be determinated."
715
  (loop for (type dir func) in rails-directory<-->types
716
        when (and (rails-core:buffer-file-match dir)
717
                  (if func
718
                      (apply func (list (buffer-file-name (current-buffer))))
719
                    t))
720
        do (return type)))
721
722
723
;;;;;;;;;; Rails minor mode Buttons ;;;;;;;;;;
724
725
(define-button-type 'rails-button
726
  'follow-link t
727
  'action #'rails-core:button-action)
728
729
(defun rails-core:button-action (button)
730
  (let* ((file-name (button-get button :rails:file-name))
731
         (line-number (button-get button :rails:line-number))
732
         (file (rails-core:file file-name)))
733
    (when (and file
734
               (file-exists-p file))
735
      (find-file-other-window file)
736
      (when line-number
737
        (goto-line line-number)))))
738
739
;;;;;;;;;; Rails minor mode logs ;;;;;;;;;;
740
741
(defun rails-log-add (message)
742
  "Add MESSAGE to the Rails minor mode log in RAILS_ROOT."
743
  (rails-project:with-root
744
   (root)
745
   (append-string-to-file (rails-core:file "log/rails-minor-mode.log")
746
                          (format "%s: %s\n"
747
                                  (format-time-string "%Y/%m/%d %H:%M:%S") message))))
748
749
(defun rails-logged-shell-command (command buffer)
750
  "Execute a shell command in the buffer and write the results to
751
the Rails minor mode log."
752
  (shell-command (format "%s %s" rails-ruby-command command) buffer)
753
  (rails-log-add
754
   (format "\n%s> %s\n%s" (rails-project:name)
755
           command (buffer-string-by-name buffer))))
756
757
;;;;;;;;;; Rails menu ;;;;;;;;;;
758
759
(defun rails-core:menu-separator ()
760
  (unless (rails-use-text-menu) 'menu (list "--" "--")))
761
762
(if (fboundp 'completion-posn-at-point-as-event)
763
    (defun rails-core:menu-position ()
764
      (completion-posn-at-point-as-event nil nil nil (+ (frame-char-height) 2)))
765
  (defun rails-core:menu-position ()
766
    (list '(300 50) (get-buffer-window (current-buffer)))))
767
768
;; fixup emacs-rails menu specs to work with tmm-prompt
769
(defun rails-core:tmm-menu (menu)
770
  (symbol-name (tmm-prompt (cons (car menu)
771
				 (mapcar (lambda (pane)
772
					   (cons (car pane)
773
						 (mapcar (lambda (item)
774
							   (if (symbolp (cdr item))
775
							       item
776
							     (cons (car item)
777
								   (intern (cdr item)))))
778
							 (cdr pane))))
779
					 (cdr menu))))))
780
781
(defun rails-core:ido-menu (menu)
782
  (let* ((prompt (car (car (cdr menu))))
783
         (mappings (cdr (car (cdr menu))))
784
         (choices (delete-if #'not (mapcar (lambda (item) (car item)) mappings)))
785
         (default (if (find-if (lambda (val) (string= (word-at-point) val)) choices)
786
                    (word-at-point)))
787
         (result (ido-completing-read prompt choices nil nil default)))
788
    (or (cdr (assoc result mappings)) result)))
789
790
(defun rails-core:menu (menu)
791
  "Show a menu."
792
  (let ((result
793
         (if (rails-use-text-menu)
794
           (funcall (or rails-text-menu-function
795
                        (and (boundp 'ido-mode) ido-mode #'rails-core:ido-menu)
796
                        #'rails-core:tmm-menu) menu)
797
           (x-popup-menu (rails-core:menu-position)
798
                         (rails-core:prepare-menu menu)))))
799
    (if (listp result)
800
        (first result)
801
      result)))
802
803
(defvar rails-core:menu-letters-list
804
  (let ((res '()))
805
    (loop for i from (string-to-char "1") upto (string-to-char "9")
806
          do (add-to-list 'res (char-to-string i) t))
807
    (loop for i from (string-to-char "a") upto (string-to-char "z")
808
          do (add-to-list 'res (char-to-string i) t))
809
    res)
810
  "List contains 0-9a-z letter")
811
812
(defun rails-core:prepare-menu (menu)
813
  "Append a prefix to each label of menu-item from MENU."
814
  (let ((title (car menu))
815
        (menu (cdr menu))
816
        (result '())
817
        (result-line '())
818
        (letter 0))
819
    (dolist (line menu)
820
      (setq result-line '())
821
      (dolist (it line)
822
        (typecase it
823
          (cons
824
           (if (and (string= (car (rails-core:menu-separator)) (car it))
825
                    (string= (cadr (rails-core:menu-separator)) (cadr it)))
826
               (add-to-list 'result-line it t)
827
             (progn
828
               (add-to-list 'result-line (cons
829
                                          (format "%s) %s"
830
                                                  (nth letter rails-core:menu-letters-list)
831
                                                  (car it))
832
                                          (cdr it))
833
                            t)
834
               (setq letter (+ 1 letter)))))
835
          (t
836
           (add-to-list 'result-line it t))))
837
      (add-to-list 'result result-line t))
838
    (cons title result)))
839
840
;;;;;;;;;; Misc ;;;;;;;;;;
841
842
(defun rails-core:erb-block-string ()
843
  "Return the contents of the current ERb block."
844
  (save-excursion
845
    (save-match-data
846
      (let ((start (point)))
847
        (search-backward-regexp "<%[=]?")
848
        (let ((from (match-end 0)))
849
          (search-forward "%>")
850
          (let ((to (match-beginning 0)))
851
            (when (>= to start)
852
              (buffer-substring-no-properties from to))))))))
853
854
(defun rails-core:rhtml-buffer-p ()
855
  "Return non nil if the current buffer is rhtml file."
856
  (string-match "\\.rhtml\\|\\.html\\.erb$" (buffer-file-name)))
857
858
(defun rails-core:spec-exist-p ()
859
  "Return non nil if spec directory is exist."
860
  (file-exists-p (rails-core:file "spec")))
861
862
(provide 'rails-core)