python-el-fgallina-expansions.el (7371B)
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