Source

python-extras / python-extras.el

  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
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
;;; python-extras.el --- Extras for python-mode

;; Filename: python-extras.el
;; Description: Extras for python-mode
;; Author: Mickey Petersen (rot13 "zvpxrl@slrnu.bet")
;; Maintainer: Mickey Petersen
;; Copyright (C) 2010, Mickey Petersen, all rights reserved.
;; Created: 2010-05-22 21:21:04
;; Version: 0.2
;; Keywords: python utility refactor extras
;; Compatibility: GNU Emacs 23
;;
;; Features that might be required by this library:
;;
;; Emacs' built-in `python.el'.
;; Will not work with `python-mode.el' (yet)
;;

;;; This file is NOT part of GNU Emacs

;;; 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 3, 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; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
;; Floor, Boston, MA 02110-1301, USA.

;;; Commentary:
;;
;; Random grab bag of extras for \\[python-mode] and
;; \\[inferior-python-mode].
;;
;; This package was made to improve Emacs' existing python mode,
;; `python.el'.  Unlike packages like ropemacs this module does not have
;; any mandatory external dependencies.
;;
;;
;; Several different helper functions were added to:
;;
;;  * Let you add parameters to the function block you're editing in
;;    using the minibuffer;
;;
;;  * Send the expression under point to either 'dir' or 'help' in
;;    inferior python without disrupting your current input.
;;
;;  * Add basic syntax highlighting to inferior python.
;;
;;  * Shift regions of code around and reindents according to the
;;    indentation depth of that block
;;
;;
;;;;;; How to use
;;
;; By default several commands are bound to various 'C-c' keybinds.
;;
;; In \\[python-mode]:
;;
;;; Misc
;;
;; <RET> - Now rebound to `newline-and-indent' -- as it should be.
;;
;; C-S-<up>/<down> - Shifts a region up/down intelligently,
;; reindenting as necessary.
;;
;;;; Refactor
;;
;; C-c C-p - Calls `python-mp-add-parameter' which will prompt you for
;; a parameter to add to the function point is currently in.  If you
;; are not in a function body an error is raised.
;;
;;
;;; Extract...
;;
;; Extracts the string/s-exp/expression at point to the top of the
;; current...
;;
;;   C-c C-e C-b - block
;;
;;   C-c C-e C-d - def
;;
;;   C-c C-e C-c - class
;;
;; In inferior python mode:
;;
;;; Inferior Python
;;
;; C-c C-d - Invokes `python-mp-send-dir'.  Sends a dir(EXPR) command
;; where EXPR is the expression at point.  It will preserve your
;; current input.
;;
;; C-d C-h - Invokes `python-mp-send-help'.  Sends a help(EXPR) command
;; where EXPR is the expressio nat point.  It will also preserve your
;; current input.
;;
;; Highlighting - Strings are now highlighted using a special "safety"
;; font locker to prevent the colors from 'bleeding'.
;;
;;
;;; Installation:
;;
;; Put `python-extras.el' somewhere in your `load-path'.
;;
;; Now add the following line to your .emacs or init.el:
;;
;; (require 'python-extras)
;;
;;

;;; Change log:
;; 2010/06/23
;;      * Added experimental replacement for python's default
;;        indentation function. The new function will automatically
;;        (but na�vely) reindent code blocks.
;;
;; 2010/06/18
;;      * Added extract to def/block/class
;;
;; 2010/06/06
;;      * Added `python-mp-shift-region-up/down'. Regions can
;;        now be moved around with C-S-<up> and C-S-<down>.
;;
;; 2010/06/02
;;      * Clarified the documentation and comments.
;;
;; 2010/05/24
;;      * Added typical Emacs GPL header.
;;
;;      * Introduced the first (of many?) font-lock additions to
;;        inferior python mode.
;;
;; 2010/05/22
;;      * Added `python-mp-send-help-at-pt'
;;
;; 2010/05/21
;;      * Begun work.
;;

;;; TODO
;;
;; Lots. `python-mp-add-parameter' works fine for pathological cases
;; but would probably fail if you have an esoteric coding style.
;;
;; I'm sure there are issues with the way i get the expression at
;; point. I use `thing-at-point' -- which is great -- but I have to
;; hack the syntax table. I'm sure there's a better way. And
;; `python.el' comes with something like it baked in; maybe find a way
;; of making it work with that?
;;
;; Add support for `python-mode.el', but that'll involve lots of
;; compatibility hacks or abusing defalias to map `python.el' to
;; `python-mode.el' bindings.
;;
;; I also need to add defcustom support; there's not a whole lot to
;; customize at this point but that's bound to change.
;;
;; Incorporate `Info-mode' help generation using
;; `comint-redirect-send-command-to-process'.
;;
;; Completion support with rlcompleter2
;;
;;

;;; Require

(require 'rx)
(require 'comint)
(require 'python)
(require 'thingatpt)
(require 'skeleton)

;;; Code:


;;; Keymaps

(define-key python-mode-map (kbd "C-c C-s") 'python-mp-send-and-switch)

;; refactoring
(define-key python-mode-map (kbd "C-c C-p") 'python-mp-add-parameter)
(define-key python-mode-map (kbd "C-c C-e C-d") 'python-mp-extract-to-def)
(define-key python-mode-map (kbd "C-c C-e C-c") 'python-mp-extract-to-class)
(define-key python-mode-map (kbd "C-c C-e C-b") 'python-mp-extract-to-block)

;; this really should be the default keybinding in Python.
(define-key python-mode-map (kbd "C-m") 'newline-and-indent)

;; smart quote functionality
;; (define-key python-mode-map ?\" 'python-mp-smart-quote)
;; (define-key python-mode-map ?\' 'python-mp-smart-quote)

;; region shifting and indentation modifications
(define-key python-mode-map (kbd "C-S-<up>") 'python-mp-shift-region-up)
(define-key python-mode-map (kbd "C-S-<down>") 'python-mp-shift-region-down)
;;(define-key python-mode-map (kbd "<tab>") 'python-mp-reindent)

;;; Keymaps for inferior python
(define-key inferior-python-mode-map (kbd "C-c C-h") 'python-mp-send-help)
(define-key inferior-python-mode-map (kbd "C-c C-d") 'python-mp-send-dir)

(defconst python-mp-def-regexp (rx bol (0+ (any space)) "def")
  "Regular expression `python-mp-add-parameter' uses to match a
  function definition.")

(defconst python-mp-class-regexp (rx bol (0+ (any space)) "class")
  "Regular expression `python-mp-extract-to' uses to match a
  class definition.")

(defun python-mp-send-help ()
  "Sends a help(EXPR) command when called from an inferior python
buffer, where EXPR is an expression at Point."
  (interactive (if (eq major-mode 'inferior-python-mode)
                   (python-mp-compile-comint-query "help" (python-mp-get-expression-at-pt)))))

(defun python-mp-send-dir ()
  "Sends a dir(EXPR) command when called from an inferior python
buffer, where EXPR is an expression at Point."
  (interactive (if (eq major-mode 'inferior-python-mode)
                   (python-mp-compile-comint-query "dir" (python-mp-get-expression-at-pt)))))

(defun python-mp-get-expression-at-pt ()
  "Takes the word at point using a modified syntax table and
returns it."
  ;; we need a quick-and-dirty syntax table hack here to make
  ;; `thing-at-point' pick up on the fact that '.', '_', etc. are all
  ;; part of a single expression.
  (with-syntax-table (make-syntax-table)
    (modify-syntax-entry ?. "w")
    (modify-syntax-entry ?_ "w")
    ;; grab the word and return it
    (let ((word (thing-at-point 'word)))
      (if word
          word
        (error "Cannot find an expression at point")))))

(defun python-mp-compile-comint-query (func arg)
  "Sends a func(ARG) query to an `inferior-python-mode' process
using `python-mp-call-func-on-word'"
  ;(interactive "sFunc: \nsQuery: ")
  ;; this should only work in `inferior-python-mode'
  (if (and (eq major-mode 'inferior-python-mode)
           arg
           (stringp func)
           (stringp arg))
      (python-mp-send-func func arg)
    (error "Failed to send query")))

(defconst python-mp-from-statement-regexp "")

(defun python-mp-declare-import (module &optional identifier)
  "Not Yet Implemented. Introduces MODULE as a new import statement if
it is not already defined.

If IDENTIFIER is defined then the import statement is defined as
a \"from\" statement instead.

If the import statement already exists no action is taken; if it
does not, it is created. If IDENTIFIER is defined and MODULE
already declared, the existing \"from IMPORT\" clause is updated
with IDENTIFIER."
  (error "NYI"))

(defun python-mp-stringp (pt)
  "Return t if PT is in a Python string.

Uses `parse-partial-sexp' to infer the context of point."
  ;; Are there cases where point could be in a string but without a
  ;; string symbol?
    (eq 'string (syntax-ppss-context (syntax-ppss pt))))

(defun python-mp-commentp (pt)
  "Returns t if PT is in a Python comment."
  (eq 'comment (syntax-ppss-context (syntax-ppss pt))))

(defun python-mp-extract-dwim ()
  "Extracts the expression, string or sexp at point and returns
it."
  ;; if point is in a string we want to extract all of it.
  (cond
   ((python-mp-stringp (point))
    (save-excursion
      (python-beginning-of-string)
      (delete-and-extract-region (point) (save-excursion (forward-sexp) (point)))))
   ((python-mp-commentp (point))
    (error "Cannot use Extract Expression in a comment"))
   (t
    (let ((bounds (bounds-of-thing-at-point 'sexp)))
      (if bounds
          (delete-and-extract-region (car bounds) (cdr bounds))
        (error "Cannot find a valid expression"))))))

(defun python-mp-extract-to (name place)
  "Extracts the expression, string, or sexp using
`python-mp-extract-dwim' to a variable NAME in PLACE.

PLACE must be one of the following valid symbols: `class' for the
class point is in; `def' to add it to the top of the def
statement point is in; `block' to add it to the top of the block
point is in."
  (save-excursion
    (unless name (error "Must have a valid name"))
    (setq oldpt (point))
    (catch 'done
      (while
          (progn
            ;; blocks use `python-beginning-of-block'
            (if (eq place 'block)
                (python-beginning-of-block)
              (python-beginning-of-defun))
            ;; keep going back to the previous def or class until we
            ;; encounter the statement we're looking for. If we're
            ;; looking for a block we simply proceed without
            ;; checking at all.
            (when (or (looking-at
                       (if (eq place 'class)
                           python-mp-class-regexp
                         python-mp-def-regexp))
                      (eq place 'block))
              ;; we must do this here as we're manipulating the
              ;; buffer later on and that will throw off `oldpt'.
              (setq full-expr
                    (concat name " = "
                            (save-excursion
                              (goto-char oldpt)
                              (python-mp-extract-dwim))))
              ;; FIXME: this assumes that `end-of-line' is "end of
              ;; block"; it might not be?
              (end-of-line)
              (newline-and-indent)
              ;; stick the new expression into the buffer...
              (insert full-expr)
              ;; ... and signal the catch statement that we're done.
              (throw 'done nil))
            ;; loop condition here means we stop looking if we hit
            ;; 0'th indentation level as that's as far back as we
            ;; can go without jumping to earlier, unrelated,
            ;; statements.
            (> (python-mp-indentation-at-point (point)) 0)))
      (message "No statement found."))
    (message (concat (symbol-name place) " --> " (python-initial-text)))))

(defun python-mp-extract-to-block (name)
  "Extracts the expression, string, or sexp at point to the
nearest `block' statement."
  (interactive "sName: ")
  (python-mp-extract-to name 'block))

(defun python-mp-extract-to-class (name)
  "Extracts the expression, string, or sexp at point to the
nearest `class' statement."
  (interactive "sName: ")
  (python-mp-extract-to name 'class))

(defun python-mp-extract-to-def (name)
  "Extracts the expression, string, or sexp at point to the
nearest `def' statement."
  (interactive "sName: ")
  (python-mp-extract-to name 'def))

(defun python-mp-send-func (func arg)
  "Constructs a FUNC(ARG) request to an inferior-python process and
sends it without interrupting user input"
  (let ((proc (get-buffer-process (current-buffer))))
    (if proc
        (progn
          ;; construct a query for `python-shell' of the form 'func(arg)'.
          ;; FIXME: better way?
          (comint-send-string proc (concat func "(" arg ")" "\n"))
          ;; count the number of lines left between `point' and
          ;; `window-end'. If it this number is 0 or 1 we're at the
          ;; last line and thus we shouldn't move the point to the
          ;; very end, as the user invoked the command on
          ;; a line they're still editing.
          (if (> (count-lines (point) (window-end)) 1)
              (goto-char (point-max))))
      (error "No process found"))))

(defun python-mp-add-parameter (param)
  "Appends a parameter to the Python function point belongs to.

If there are no functions then an error is raised.
If called from within a class -- but outside a method body -- an error is raised."
  (interactive "sParameter: ")
  (save-excursion
    (save-restriction
      (widen)
      ;; point is now at the beginning of defun.
      ;;
      ;; FIXME: This would obviously fail in `python-mode.el'. There's
      ;; no function by that name there as far as i know.
      (python-beginning-of-defun)
      ;; only defs can have parameters.
      (if (not (looking-at python-mp-def-regexp))
          (error "Can only add parameters to functions"))

      ;; find the opening parenthesis for the parameter list then move
      ;; forward one s-expression so we end up at the end. We *could*
      ;; search for ':' instead then go back but if someone were to
      ;; use that character as a default parameter value that would
      ;; fail.
      (search-forward "(")
      (backward-char)
      (forward-sexp)
      ;; if we have an empty parameter list we simply go back one char
      ;; to enter the expression
      (if (looking-back (rx "(" (0+ (any space)) ")"))
          (progn
            ;; jump back to the beginning of the expression then
            ;; forward one char - that should put us right inside the
            ;; expression
            (goto-char (match-beginning 0))
            (forward-char))
        ;; ... but if the expression isn't empty we simply move
        ;; backwards one character.
        (backward-char)
        (if (re-search-backward (rx (not (any "(" space))))
            (progn
              (forward-char)
              (insert ", "))
          (error "Cannot find a valid parameter field")))
      (insert param)
      ;; Show the modified line in the minibuffer.
      ;;
      ;; FIXME: if it's somehow spread out over multiple lines we'll
      ;; only get the line we added our own parameter to. I guess
      ;; that's OK?
      (message (python-initial-text)))))

; Sends the contents of the buffer then switches to the python buffer.
(defun python-mp-send-and-switch ()
  "Sends the current buffer to Python but only after moving point
to the end of the buffer. After that it switches back to the
inferior python buffer."
  (interactive)
  (let ((currshell python-buffer)
	(currbuffer (buffer-name)))
    ;; switch to python so we can jump to end-of-buffer.
    (with-selected-window (selected-window)
      (python-switch-to-python currshell)
      (goto-char (point-max)))
    ;; we're back in the python buffer. send the output then switch
    ;; back to the shell again
    (python-send-buffer)
    (python-switch-to-python currshell)
    (message (concat "Sent python buffer " currbuffer  " at " (current-time-string)))))

(defun python-mp-smart-quote ()
  "Inserts another pair of quotes -- either single or double -- if point is on a quote symbol."
  (error "NYI")
  )

(defun python-mp-indentation-at-point (pt)
  "Determines the indentation at PT. This approach does not use
\\[python-mode]'s internal data structures as we're not
interested in the *possible* indentation levels but merely what
PT currently has.

If the line contains nothing but whitespace (as determined by the
syntax table) then and `indent-count' is 0 we recursively
backtrack one line at a time until that condition is no longer
satisfied."
  (save-excursion
    (unless (bobp)
      (goto-char pt)
      (beginning-of-line)
      (setq indent-count
            (- (progn
                 ;; `line-end-position' seems like the best way to
                 ;; limit the search; but is it enough?
                 (skip-syntax-forward " " (line-end-position))
                 (point))
               (point-at-bol)))
      ;; FIXME: can `indent-count' ever be less than 0?
      ;;
      ;; make sure we're eolp also or we run into the nasty situation
      ;; where `indent-count' is 0 and yet the line contains code.
      (if (and (= indent-count 0) (eolp))
          (progn
            (forward-line -1)
            (python-mp-indentation-at-point (point)))
        indent-count))))


;;FIXME: this is a bit hacky...
(defun python-mp-reindent ()
  "Reindents the active region if \\[transient-mark-mode] is on."
  (interactive)
  (if (region-active-p)
      ;; shift the region by 0 lines which means it'll stay where it
      ;; is but reindent.
      (progn
        (python-mp-shift-region 0 'smart)
        (deactivate-mark))
    ;; there is a special place in hell reserved for people who alter
    ;; `this-command' and `last-command'.
    (let ((this-command 'indent-for-tab-command)
          (last-command 'indent-for-tab-command))
      ;; default to the usual python-mode indentation function.
      (indent-for-tab-command))))

(defun python-mp-shift-region (arg subr)
  "Shifts the active region ARG times up (backward if ARG is
negative) and reindents the code according to the indentation
depth in that block if SUBR is `'smart'. "
  ;;; Code loosely based off code snarfed from Andreas Politz on
  ;;; `gnu.emacs.help'.
  (progn
    ;; if there's no region active we should act on the entire line
    ;; instead. That avoids the uncomfortable situation where mark is
    ;; *somewhere* in the buffer and shifting the region would move
    ;; the region from point to mark.
    (unless (region-active-p)
      (set-mark
       (save-excursion
         (forward-line 0)
         (point)))
      (end-of-line)
      ;; we must have this or we won't include the newline.
      (forward-char)
      (activate-mark))
    (if (> (point) (mark))
        (exchange-point-and-mark))
    (let ((column (current-column))
          (text (delete-and-extract-region (point) (mark))))
      ;; FIXME: this sorta breaks the undo ring if you shift a
      ;; region and then immediately `undo'. This needs to be
      ;; fixed. The workaround is to do something to add to the
      ;; undo-ring (like movement) then it'll work fine.
      (forward-line arg)
      (move-to-column column t)
      (set-mark (point))
      (insert text)
      ;; without this point would be at the end of the region
      (exchange-point-and-mark)
      (if (eq subr 'smart)
          (progn
            (indent-rigidly (point) (mark)
                            ;; the inner-most block indentation level
                            ;; is what we're after. Subtract the
                            ;; current indentation at point (the
                            ;; top-most line in the region) from it
                            ;; to get the amount we need to rigidly
                            ;; indent by.
                            (- (caar (last (python-indentation-levels)))
                               (python-mp-indentation-at-point (point))))))
      (setq deactivate-mark nil))))

(defun python-mp-shift-region-down (arg)
  "If the region is active and \\[transient-mark-mode] is enabled
the region will be shifted down ARG times and reindented."
  (interactive "*p")
  (python-mp-shift-region arg 'smart))

(defun python-mp-shift-region-up (arg)
  "If the region is active and \\[transient-mark-mode] is enabled
the region will be shifted up ARG times and reindented."
  (interactive "*p")
  (python-mp-shift-region (- arg) 'smart))

;;; inferior-python-mode

;; `python-mode' enhancements.
(font-lock-add-keywords 'inferior-python-mode
  `(
    ;; rudimentary string handler routine. I could snarf the one used
    ;; by `python-mode' but I don't think it would make much
    ;; sense. This one has the added advantage of making it very
    ;; difficult for `font-lock-string-face' to "bleed" if a closing
    ;; quote character is missing.
    (,(rx (group (any "\"'"))
         (*? nonl)
         (backref 1)) . font-lock-string-face)))


(provide 'python-extras)

;;; python-extras.el ends here
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.