/ python-el-fgallina-expansions.el
python-el-fgallina-expansions.el
  1  ;;; python-el-fgallina-expansions.el --- fgallina/python.el-specific expansions for expand-region  -*- lexical-binding: t; -*-
  2  
  3  ;; Copyright (C) 2012-2023  Free Software Foundation, Inc
  4  
  5  ;; Author: Felix Geller
  6  ;; Keywords: marking region python
  7  
  8  ;; This program is free software; you can redistribute it and/or modify
  9  ;; it under the terms of the GNU General Public License as published by
 10  ;; the Free Software Foundation, either version 3 of the License, or
 11  ;; (at your option) any later version.
 12  
 13  ;; This program is distributed in the hope that it will be useful,
 14  ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
 15  ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16  ;; GNU General Public License for more details.
 17  
 18  ;; You should have received a copy of the GNU General Public License
 19  ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
 20  
 21  ;;; Commentary:
 22  ;;
 23  ;;  - Additions implemented here:
 24  ;;    - `er/mark-inside-python-string'
 25  ;;    - `er/mark-outside-python-string'
 26  ;;    - `er/mark-python-statement'
 27  ;;    - `er/mark-python-block'
 28  ;;    - `er/mark-outer-python-block'
 29  ;;    - `er/mark-python-block-and-decorator'
 30  ;;  - Supports multi-line strings
 31  
 32  ;;; Code:
 33  
 34  (require 'expand-region-core)
 35  
 36  (if (not (fboundp 'python-syntax-context))
 37      (defalias 'python-syntax-context #'python-info-ppss-context))
 38  (if (not (fboundp 'python-indent-offset))
 39      (defalias 'python-indent-offset #'python-indent))
 40  
 41  (defvar er--python-string-delimiter
 42    "'\""
 43    "Characters that delimit a Python string.")
 44  
 45  ;; copied from @fgallina's python.el as a quick fix. The variable
 46  ;; `python-rx-constituents' is not bound when we use the python-rx
 47  ;; macro from here, so we have to construct the regular expression
 48  ;; manually.
 49  (defvar er--python-block-start-regex
 50    (rx symbol-start
 51        (or "def" "class" "if" "elif" "else" "try"
 52            "except" "finally" "for" "while" "with")
 53        symbol-end)
 54    "Regular expression string to match the beginning of a Python block.")
 55  
 56  (defun er/mark-python-string (mark-inside)
 57    "Mark the Python string that surrounds point.
 58  
 59  If the optional MARK-INSIDE is not nil, only mark the region
 60  between the string delimiters, otherwise the region includes the
 61  delimiters as well."
 62    (let ((beginning-of-string (python-syntax-context 'string (syntax-ppss))))
 63      (when beginning-of-string
 64        (goto-char beginning-of-string)
 65        ;; Move inside the string, so we can use ppss to find the end of
 66        ;; the string.
 67        (skip-chars-forward er--python-string-delimiter)
 68        (while (python-syntax-context 'string (syntax-ppss))
 69          (forward-char 1))
 70        (when mark-inside (skip-chars-backward er--python-string-delimiter))
 71        (set-mark (point))
 72        (goto-char beginning-of-string)
 73        (when mark-inside (skip-chars-forward er--python-string-delimiter)))))
 74  
 75  (defun er/mark-inside-python-string ()
 76    "Mark the inside of the Python string that surrounds point.
 77  
 78  Command that wraps `er/mark-python-string'."
 79    (interactive)
 80    (er/mark-python-string t))
 81  
 82  (defun er/mark-outside-python-string ()
 83    "Mark the outside of the Python string that surrounds point.
 84  
 85  Command that wraps `er/mark-python-string'."
 86    (interactive)
 87    (er/mark-python-string nil))
 88  
 89  (defun er/mark-python-statement ()
 90    "Mark the Python statement that surrounds point."
 91    (interactive)
 92    (python-nav-end-of-statement)
 93    (set-mark (point))
 94    (python-nav-beginning-of-statement))
 95  
 96  (defun er/mark-python-block (&optional next-indent-level)
 97    "Mark the Python block that surrounds point.
 98  
 99  If the optional NEXT-INDENT-LEVEL is given, select the
100  surrounding block that is defined at an indentation that is less
101  than NEXT-INDENT-LEVEL."
102    (interactive)
103    (back-to-indentation)
104    (let ((next-indent-level
105           (or
106            ;; Use the given level
107            next-indent-level
108            ;; Check whether point is at the start of a Python block.
109            (if (looking-at er--python-block-start-regex)
110                ;; Block start means that the next level is deeper.
111                (+ (current-indentation) python-indent-offset)
112              ;; Assuming we're inside the block that we want to mark
113              (current-indentation)))))
114      ;; Move point to next Python block start at the correct indent-level
115      (while (>= (current-indentation) next-indent-level)
116        (re-search-backward er--python-block-start-regex))
117      ;; Mark the beginning of the block
118      (set-mark (point))
119      ;; Save indentation and look for the end of this block
120      (let ((block-indentation (current-indentation)))
121        (forward-line 1)
122        (while (and
123                ;; No need to go beyond the end of the buffer. Can't use
124                ;; eobp as the loop places the point at the beginning of
125                ;; line, but eob might be at the end of the line.
126                (not (= (point-max) (line-end-position)))
127                ;; Proceed if: indentation is too deep
128                (or (> (current-indentation) block-indentation)
129                    ;; Looking at an empty line
130                    (looking-at (rx line-start (* whitespace) line-end))
131                    ;; We're not looking at the start of a Python block
132                    ;; and the indent is deeper than the block's indent
133                    (and (not (looking-at er--python-block-start-regex))
134                         (> (current-indentation) block-indentation))))
135          (forward-line 1)
136          (back-to-indentation))
137        ;; Find the end of the block by skipping comments backwards
138        (python-util-forward-comment -1)
139        (exchange-point-and-mark))))
140  
141  (defun er/mark-outer-python-block ()
142    "Mark the Python block that surrounds the Python block around point.
143  
144  Command that wraps `er/mark-python-block'."
145    (interactive)
146    (er/mark-python-block (current-indentation)))
147  
148  (defun er/mark-python-block-and-decorator ()
149    (interactive)
150    (back-to-indentation)
151    (if (or (er--python-looking-at-decorator) (er--python-looking-at-decorator -1))
152        (progn
153  	(while (er--python-looking-at-decorator -1)
154  	  (forward-line -1)
155  	  (back-to-indentation)
156  	  )
157  	(set-mark (point))
158  	(while (er--python-looking-at-decorator)
159  	  (forward-line)
160  	  )
161  	(python-nav-end-of-block)
162  	(exchange-point-and-mark))))
163  
164  (defun er--python-looking-at-decorator (&optional line-offset)
165    (save-excursion
166      (if line-offset
167  	(forward-line line-offset)
168  	)
169      (back-to-indentation)
170      (looking-at "@")
171      ))
172  
173  (defun er/add-python-mode-expansions ()
174    "Adds python-mode-specific expansions for buffers in python-mode"
175    (let ((try-expand-list-additions '(
176                                       er/mark-inside-python-string
177                                       er/mark-outside-python-string
178                                       er/mark-python-statement
179                                       er/mark-python-block
180  				     er/mark-python-block-and-decorator
181                                       er/mark-outer-python-block
182                                       )))
183      (set (make-local-variable 'expand-region-skip-whitespace) nil)
184      (set (make-local-variable 'er/try-expand-list)
185           (remove 'er/mark-inside-quotes
186                   (remove 'er/mark-outside-quotes
187                           (append er/try-expand-list try-expand-list-additions))))))
188  
189  (er/enable-mode-expansions 'python-mode #'er/add-python-mode-expansions)
190  
191  (provide 'python-el-fgallina-expansions)
192  
193  ;; python-el-fgallina-expansions.el ends here