Source

vim-mode / vim-ex.el

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
;;; vim-ex.el - Ex-mode.

;; Copyright (C) 2009, 2010 Frank Fischer

;; Author: Frank Fischer <frank.fischer@mathematik.tu-chemnitz.de>,
;;
;; This file is not part of GNU Emacs.

;;; Code:
(provide 'vim-ex)

(defvar vim:ex-commands nil
  "List of pairs (command . function).")

(defvar vim:ex-current-buffer)

(vim:deflocalvar vim:ex-local-commands nil
  "List of pairs (command . function).")

(defvar vim:ex-history nil
  "History of ex-commands.")

(defun vim:emap (keys command)
  "Maps an ex-command to some function."
  (unless (find-if #'(lambda (x) (string= (car x) keys)) vim:ex-commands)
    (add-to-list 'vim:ex-commands (cons keys command))))

(defun vim:local-emap (keys command)
  "Maps an ex-command to some function buffer-local."
  (unless (find-if #'(lambda (x) (string= (car x) keys)) vim:ex-local-commands)
    (add-to-list 'vim:ex-local-commands (cons keys command))))

(defun vim:ex-binding (cmd)
  "Returns the current binding of `cmd' or nil."
  (with-current-buffer vim:ex-current-buffer
    (or (cdr-safe (assoc cmd vim:ex-local-commands))
        (cdr-safe (assoc cmd vim:ex-commands)))))

(defvar vim:ex-keymap (make-sparse-keymap)
  "Keymap used in ex-mode.")

(define-key vim:ex-keymap "\t" 'minibuffer-complete)
(define-key vim:ex-keymap [return] 'exit-minibuffer)
(define-key vim:ex-keymap (kbd "RET") 'exit-minibuffer)
(define-key vim:ex-keymap " " 'vim:ex-expect-argument)
(define-key vim:ex-keymap (kbd "C-j") 'vim:ex-execute-command)
(define-key vim:ex-keymap (kbd "C-g") 'abort-recursive-edit)
(define-key vim:ex-keymap [up] 'previous-history-element)
(define-key vim:ex-keymap [down] 'next-history-element)

(defvar vim:ex-keep-reading nil)
(defvar vim:ex-cmdline nil)
(defvar vim:ex-cmd nil)
(defvar vim:ex-beg nil)
(defvar vim:ex-end nil)

(defun vim:ex-split-cmdline (cmdline)
  (multiple-value-bind (cmd-region beg end) (vim:ex-parse cmdline)
    (if (null cmd-region)
        (values "" "" cmdline "" nil nil)
      (let ((range (substring cmdline 0 (car cmd-region)))
            (cmd (substring cmdline (car cmd-region) (cdr cmd-region)))
            (spaces "")
            (arg (substring cmdline (cdr cmd-region))))
    
        ;; skip whitespaces
        (when (string-match "\\`\\s-*" arg)
          (setq spaces (match-string 0 arg)
                arg (substring arg (match-end 0))))
      
        (values range cmd spaces arg beg end)))))

(defun vim:ex-expect-argument (n)
  ;; called if the space separating the command from the argument has
  ;; been pressed
  (interactive "p")
  (let ((cmdline (vim:minibuffer-contents)))
    (self-insert-command n)
    (multiple-value-bind (range cmd spaces arg beg end) (vim:ex-split-cmdline cmdline)

      (when (and (= (point) (point-max))
                 (zerop (length spaces))
                 (zerop (length arg)))
        (while (stringp cmd)
          (setq cmd (vim:ex-binding cmd)))
        
        (if (null cmd) (ding)
          (let ((result (case (vim:cmd-arg cmd)
                          (file
                           (vim:ex-complete-file-argument nil nil nil))
                          (buffer
                           (vim:ex-complete-buffer-argument nil nil nil))
                          ((t)
                           (vim:ex-complete-text-argument nil nil nil)))))
            (when result (insert result))))))))
          
(defun vim:ex-complete (cmdline predicate flag)
  (multiple-value-bind (range cmd spaces arg beg end) (vim:ex-split-cmdline cmdline)
    (setq vim:ex-cmd cmd)

    (cond
     ;; only complete at the end of the command
     ((< (point) (point-max)) nil)
       
     ;; if at the end of a command, complete the command
     ((and (zerop (length spaces)) (zerop (length arg)))
      (let ((result (vim:ex-complete-command cmd predicate flag)))
        (cond
         ((null result) nil)
         ((eq t result) t)
         ((stringp result)
          (if flag result (concat range result)))
         ((listp result) (if flag result (map #'(lambda (x) (concat range x)) result)))
         (t (error "Completion returned unexpected value.")))))
              
     ;; otherwise complete the argument
     (t 
      (let ((result (vim:ex-complete-argument arg predicate flag)))
        (cond
         ((null result) nil)
         ((eq t result) t)
         ((stringp result) (if flag result (concat range cmd spaces result)))
         ((listp result) (if flag result (map #'(lambda (x) (concat range cmd spaces x)) result)))
         (t (error "Completion returned unexpected value."))))))))

        
(defun vim:ex-complete-command (cmd predicate flag)
  ;; completes the command
  (with-current-buffer vim:ex-current-buffer
    (cond
     ((null flag) (or (try-completion cmd vim:ex-local-commands predicate)
                      (try-completion cmd vim:ex-commands predicate)))
   
     ((eq t flag) (or (all-completions cmd vim:ex-local-commands predicate)
                      (all-completions cmd vim:ex-commands predicate)))
   
     ((eq 'lambda flag) (or (vim:test-completion cmd vim:ex-local-commands predicate)
                            (vim:test-completion cmd vim:ex-commands predicate))))))

(defun vim:ex-complete-argument (arg predicate flag)
  ;; completes the argument
  (let ((cmd vim:ex-cmd))
    (while (stringp cmd)
      (setq cmd (vim:ex-binding cmd)))

    (if (null cmd) (ding)
      (case (vim:cmd-arg cmd)
        (file
         (vim:ex-complete-file-argument arg predicate flag))
        (buffer
         (vim:ex-complete-buffer-argument arg predicate flag))
        ((t)
         (vim:ex-complete-text-argument arg predicate flag))
        (t (ding))))))

(defun vim:ex-complete-file-argument (arg predicate flag)
  ;; completes a file-name
  (if (null arg)
      default-directory
    (let ((dir (or (file-name-directory arg)
                   (with-current-buffer vim:ex-current-buffer default-directory)))
          (fname (file-name-nondirectory arg)))
      (cond
       ((null dir) (ding))
       ((null flag)
        (let ((result (file-name-completion fname dir)))
	  (case result
	    ((nil) nil)
	    ((t) t)
	    (t (concat dir result)))))
       
       ((eq t flag) 
        (file-name-all-completions fname dir))
       
       ((eq 'lambda flag)
        (eq (file-name-completion fname dir) t))))))
      
(defun vim:ex-complete-buffer-argument (arg predicate flag)
  ;; completes a buffer name
  (when arg
    (let ((buffers (mapcar #'(lambda (buffer) (cons (buffer-name buffer) nil)) (buffer-list t))))
      (cond
       ((null flag)
        (try-completion arg buffers predicate))
       ((eq t flag) 
        (all-completions arg buffers predicate))
       ((eq 'lambda flag)
        (vim:test-completion arg buffers predicate))))))

(defun vim:ex-complete-text-argument (arg predicate flag)
  ;; completes an arbitrary text-argument
  (when arg
    (case flag
      ((nil) t)
      ((t) (list arg))
      ('lambda t))))

(defun vim:ex-execute-command (cmdline)
  (interactive)

  (multiple-value-bind (range cmd spaces arg beg end) (vim:ex-split-cmdline cmdline)
    (setq vim:ex-cmd cmd)
    
    (let ((cmd vim:ex-cmd)
          (motion (cond
                   ((and beg end)
                    (vim:make-motion :begin (save-excursion
                                              (goto-line beg)
                                              (line-beginning-position))
                                     :end (save-excursion
                                            (goto-line end)
                                            (line-beginning-position))
                                     :has-begin t
                                     :type 'linewise))
                   (beg
                    (vim:make-motion :begin (save-excursion
                                              (goto-line beg)
                                              (line-beginning-position))
                                     :end (save-excursion
                                            (goto-line beg)
                                            (line-beginning-position))
                                     :has-begin t
                                     :type 'linewise))))
          (count (and (not end) beg)))
      
      (while (stringp cmd)
        (setq cmd (vim:ex-binding cmd)))

      (when (zerop (length arg))
        (setq arg nil))

      (with-current-buffer vim:ex-current-buffer
        (if cmd
            (case (vim:cmd-type cmd)
              ('complex
               (if (vim:cmd-arg-p cmd)
                   (funcall cmd :motion motion :argument arg)
                 (funcall cmd :motion motion)))
              ('simple
               (when end
                 (error "Command does not take a range: %s" vim:ex-cmd))
               (if (vim:cmd-arg-p cmd)
                   (if (vim:cmd-count-p cmd)
                       (funcall cmd :count beg :argument arg)
                     (funcall cmd :argument arg))
                 (if (vim:cmd-count-p cmd)
                     (funcall cmd :count count)
                   (funcall cmd))))
              (t (error "Unexpected command-type bound to %s" vim:ex-cmd)))
          (ding))))))
    

;; parser for ex-commands
(defun vim:ex-parse (text)
  "Extracts the range-information from `text'.
Returns a list of up to three elements: (cmd beg end)"
  (let (begin
        (begin-off 0)
        sep
        end
        (end-off 0)
        (pos 0)
        (cmd nil))
    
    (multiple-value-bind (beg npos) (vim:ex-parse-address text pos)
      (when npos
        (setq begin beg
              pos npos)))

    (multiple-value-bind (off npos) (vim:ex-parse-offset text pos)
      (when npos
        (unless begin (setq begin 'current-line))
        (setq begin-off off
              pos npos)))

    (when (and (< pos (length text))
               (or (= (aref text pos) ?\,)
                   (= (aref text pos) ?\;)))
      (setq sep (aref text pos))
      (incf pos)
      (multiple-value-bind (e npos) (vim:ex-parse-address text pos)
        (when npos
          (setq end e
          pos npos)))
      
      (multiple-value-bind (off npos) (vim:ex-parse-offset text pos)
        (when npos
          (unless end (setq end 'current-line))
          (setq end-off off
          pos npos))))

    ;; handle the special '%' range
    (when (or (eq begin 'all) (eq end 'all))
      (setq begin 'first-line
            begin-off 0
            end 'last-line
            end-off 0
            sep ?,))
    
    (when (= pos (or (string-match "[a-zA-Z0-9!]+" text pos) -1))
      (setq cmd (cons (match-beginning 0) (match-end 0))))
               
    (multiple-value-bind (start end) (vim:ex-get-range (and begin (cons begin begin-off)) sep (and end (cons end end-off)))
      (values cmd start end))))


(defun vim:ex-parse-address (text pos)
  (cond
   ((>= pos (length text)) nil)
   
   ((= pos (or (string-match "[0-9]+" text pos) -1))
    (values (cons 'abs (string-to-number (match-string 0 text)))
            (match-end 0)))

   ((= (aref text pos) ?\%)
    (values 'all (1+ pos)))
    
   ((= (aref text pos) ?.)
    (values 'current-line (1+ pos)))

   ((= (aref text pos) ?')
    (if (>= (1+ pos) (length text))
        nil
      (values `(mark ,(aref text (1+ pos))) (+ 2 pos))))

   ((= (aref text pos) ?/)
    (when (string-match "\\([^/]+\\|\\\\.\\)\\(?:/\\|$\\)"
                        text (1+ pos))
      (values (cons 're-fwd (match-string 1 text))
              (match-end 0))))
   
   ((= (aref text pos) ??)
    (when (string-match "\\([^?]+\\|\\\\.\\)\\(?:?\\|$\\)"
                        text (1+ pos))
      (values (cons 're-bwd (match-string 1 text))
              (match-end 0))))
   
   ((and (= (aref text pos) ?\\)
         (< pos (1- (length text))))
    (case (aref text (1+ pos))
      (?/ (values 'next-of-prev-search (1+ pos)))
      (?? (values 'prev-of-prev-search (1+ pos)))
      (?& (values 'next-of-prev-subst (1+ pos)))))

   (t nil)))


(defun vim:ex-parse-offset (text pos)
  (let ((off nil))
    (while (= pos (or (string-match "\\([-+]\\)\\([0-9]+\\)?" text pos) -1))
      (if (string= (match-string 1 text) "+")
          (setq off (+ (or off 0) (if (match-beginning 2)
                                      (string-to-number (match-string 2 text))
                                    1)))
                
        (setq off (- (or off 0) (if (match-beginning 2)
                                    (string-to-number (match-string 2 text))
                                  1))))
      (setq pos (match-end 0)))
    (and off (values off pos))))
     

(defun vim:ex-get-range (start sep end)
  (with-current-buffer vim:ex-current-buffer
    (when start
      (setq start (vim:ex-get-line start)))

    (when (and sep end)
      (save-excursion
        (when (= sep ?\;) (goto-line start))
        (setq end (vim:ex-get-line end))))
  
    (values start end)))


(defun vim:ex-get-line (address)
  (let ((base (car address))
        (offset (cdr address)))
    
    (cond
     ((null base) nil)
     ((consp offset)
      (let ((line (vim:ex-get-line (car address))))
        (when line
        (save-excursion
          (goto-line line)
          (vim:ex-get-line (cdr address))))))
     
     (t
      (+ offset
         (case (or (car-safe base) base)
         (abs (cdr base))
           
         ;; TODO: (1- ...) may be wrong if the match is the empty string
         (re-fwd (save-excursion
                   (beginning-of-line 2)
                   (and (re-search-forward (cdr base))
                        (line-number-at-pos (1- (match-end 0))))))
           
         (re-bwd (save-excursion
                   (beginning-of-line 0)
                   (and (re-search-backward (cdr base))
                        (line-number-at-pos (match-beginning 0)))))
           
         (current-line (line-number-at-pos (point)))
         (first-line (line-number-at-pos (point-min)))
         (last-line (line-number-at-pos (point-max)))
         (mark (line-number-at-pos (vim:get-local-mark (cadr base))))
         (next-of-prev-search (error "Next-of-prev-search not yet implemented."))
         (prev-of-prev-search (error "Prev-of-prev-search not yet implemented."))
         (next-of-prev-subst (error "Next-of-prev-subst not yet implemented."))
         (t (error "Invalid address: %s" address))))))))


(defun vim:ex-read-command (&optional initial-input)
  "Starts ex-mode."
  (interactive)
  (let ((vim:ex-current-buffer (current-buffer)))
    (let ((minibuffer-local-completion-map vim:ex-keymap))
      (let ((result (completing-read ":" 'vim:ex-complete nil nil initial-input  'vim:ex-history)))
        (when result
          (vim:ex-execute-command result))))))

;;; vim-ex.el ends here