├── .ert-runner ├── .gitignore ├── Cask ├── Makefile ├── README.org ├── TODO.org ├── multi-line-candidate.el ├── multi-line-cycle.el ├── multi-line-decorator.el ├── multi-line-enter.el ├── multi-line-find.el ├── multi-line-highlight.el ├── multi-line-respace.el ├── multi-line-shared.el ├── multi-line.el └── test ├── multi-line-elisp-test.el ├── multi-line-python-test.el ├── multi-line-ruby-test.el └── multi-line-test.el /.ert-runner: -------------------------------------------------------------------------------- 1 | -L . 2 | -l ./test/multi-line-test.el 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | /.cask 3 | /README.html 4 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | 4 | (package-file "multi-line.el") 5 | (depends-on "ert-runner") 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CASK = cask 2 | 3 | SRCS = $(shell find . -maxdepth 1 -name '*.el') 4 | OBJECTS = $(SRCS:.el=.elc) 5 | EMACS = $(shell sh -c 'type -P emacs') 6 | 7 | .PHONY: test compile clean install 8 | 9 | .cask: 10 | $(CASK) install 11 | 12 | $(OBJECTS): .cask 13 | $(CASK) build 14 | 15 | compile: $(OBJECTS) 16 | 17 | install: compile 18 | 19 | clean-elc: 20 | rm -f $(OBJECTS) 21 | 22 | clean: clean-elc 23 | rm -rf .cask/ 24 | 25 | test: $(OBJECTS) 26 | cask exec ert-runner -L $(PWD) 27 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | # -*- mode: org; -*- 2 | 3 | #+HTML_HEAD: 4 | #+HTML_HEAD: 5 | 6 | #+HTML_HEAD: 7 | #+HTML_HEAD: 8 | #+HTML_HEAD: 9 | #+HTML_HEAD: 10 | 11 | [[https://travis-ci.org/IvanMalison/multi-line][file:https://travis-ci.org/IvanMalison/multi-line.svg?branch=master]] 12 | [[http://melpa.org/#/multi-line][file:https://melpa.org/packages/multi-line-badge.svg]] 13 | [[https://stable.melpa.org/#/multi-line][file:https://stable.melpa.org/packages/multi-line-badge.svg]] 14 | 15 | * Demo 16 | [[https://asciinema.org/a/dwft2l94f75x9l46wmdhbm5lh?t=4][https://asciinema.org/a/dwft2l94f75x9l46wmdhbm5lh.png]] 17 | 18 | * About 19 | multi-line aims to provide a flexible framework for automatically 20 | multi-lining and single-lining function invocations and definitions, 21 | array and map literals and more. It relies on functions that are 22 | defined on a per major mode basis wherever it can so that it operates 23 | correctly across many different programming languages. 24 | 25 | * Supported Languages 26 | The following languages are officially supported by multi-line 27 | - C/C++ 28 | - clojure 29 | - emacs-lisp 30 | - golang 31 | - java 32 | - javascript 33 | - python 34 | - ruby 35 | - rust 36 | - scala 37 | 38 | It is likely that multi-line will function pretty well in any language using 39 | typical brace/parenthesis/comma syntax, provided that the major mode has 40 | properly defined ~forward-sexp~ and ~indent-line~. If you find that multi-line 41 | works well without modification in your language of choice please file an issue 42 | or submit a pull request to have it added to the list of officially supported 43 | languages. 44 | * Installation 45 | Install from MELPA with ~M-x package-install multi-line~. See the [[https://github.com/milkypostman/melpa][melpa 46 | repository]] for details about how to set up MELPA if you have not already done 47 | so. 48 | * Setup 49 | #+BEGIN_SRC emacs-lisp 50 | (require 'multi-line) 51 | (global-set-key (kbd "C-c d") 'multi-line) 52 | #+END_SRC 53 | * Usage 54 | Execute the ~multi-line~ command (with M-x or a keybinding) inside the body of 55 | the expression you wish to multi-line, as show in the demo above. Invoking 56 | multi-line multiple times on the same definition will cycle between the 57 | different respacing styles that are configured for the current buffer. 58 | 59 | When invoked with a prefix argument (~C-u~), multi-line will "single-line" the 60 | expression at point, removing all new lines and replacing them with spaces (or 61 | the character configured for single lining). 62 | * Configuration 63 | multi-line can be configured to behave differently depending on the major mode 64 | of the current-buffer. The behavior of multi-line is described with a 65 | multi-line-strategy object that has three components: a find-strategy, an 66 | enter-strategy and a replace-strategy. The ~multi-line-defhook~ macro can be 67 | used to set a major mode specific multi-lining strategy. The strategy defined 68 | with the defhook will become active in any buffers with the specified mode. 69 | 70 | In the following example we set the default multi-lining behavior for the 71 | clojure programming language. 72 | 73 | #+BEGIN_SRC emacs-lisp 74 | (multi-line-defhook clojure 75 | (make-instance multi-line-strategy 76 | :find (make-instance multi-line-forward-sexp-find-strategy 77 | :split-regex "[[:space:]\n]+" 78 | :done-regex "[[:space:]]*)]}" 79 | :split-advance-fn 'multi-line-lisp-advance-fn) 80 | :respace multi-line-lisp-respacer)) 81 | #+END_SRC 82 | 83 | This expands to 84 | 85 | #+BEGIN_SRC emacs-lisp 86 | (eval-and-compile 87 | (defvar multi-line-clojure-strategy) 88 | (setq multi-line-clojure-strategy 89 | (make-instance multi-line-strategy 90 | :find 91 | (make-instance multi-line-forward-sexp-find-strategy 92 | :split-regex "[[:space:]]+" 93 | :done-regex "[[:space:]]*)]}" 94 | :split-advance-fn 'multi-line-lisp-advance-fn) 95 | :respace multi-line-lisp-respacer)) 96 | (defun multi-line-clojure-mode-hook nil 97 | (setq-local multi-line-current-strategy multi-line-clojure-strategy)) 98 | (add-hook 'clojure-mode-hook 'multi-line-clojure-mode-hook t)) 99 | #+END_SRC 100 | 101 | Users will most often want to configure the respacing portion of the multi-line 102 | strategy. If you prefer to always add a newline at every available split point 103 | you might set up the default multi-line strategy as follows: 104 | 105 | #+BEGIN_SRC emacs-lisp 106 | (setq-default multi-line-current-strategy 107 | (multi-line-strategy 108 | :respace (multi-line-default-respacers 109 | (make-instance multi-line-always-newline)))) 110 | #+END_SRC 111 | ** Built-in Mode Specific Behavior 112 | multi-line has some built in mode specific behavior that is enabled by default. 113 | The interactive function ~multi-line-disable-mode-hooks~ disables this mode 114 | specific behavior. 115 | -------------------------------------------------------------------------------- /TODO.org: -------------------------------------------------------------------------------- 1 | * DONE Account for characters AFTER last respace position when respacing to a fill column 2 | :LOGBOOK: 3 | - State "DONE" from "TODO" [2016-08-24 Wed 16:24] 4 | - State "TODO" from [2016-08-22 Mon 15:50] 5 | :END: 6 | * DONE Handle forced grouping of grouped multi-line targets as with keywords in lisp 7 | :LOGBOOK: 8 | - State "DONE" from "TODO" [2016-08-26 Fri 23:55] 9 | - State "TODO" from [2016-08-22 Mon 15:51] 10 | :END: 11 | #+BEGIN_SRC emacs-lisp 12 | (multi-line-forward-sexp-find-strategy 13 | :split-regex "[[:space:]\n]+" 14 | :done-regex "[[:space:]]*)}]" 15 | :split-advance-fn 'multi-line-lisp-advance-fn) 16 | #+END_SRC 17 | * DONE Cycle through different respace strategies with multiple multi-line invocations 18 | :LOGBOOK: 19 | - State "DONE" from "TODO" [2016-08-22 Mon 17:12] 20 | - State "TODO" from [2016-08-22 Mon 15:52] 21 | :END: 22 | * DONE Handle applying multi-line strategies in derived modes 23 | :LOGBOOK: 24 | - State "DONE" from "TODO" [2016-08-23 Tue 13:58] 25 | - State "TODO" from [2016-08-22 Mon 16:03] 26 | :END: 27 | Should multi-line-strategy just be made buffer local? 28 | Seems like that would make things much simpler 29 | * TODO "Aggressive" fill multi-lining 30 | :LOGBOOK: 31 | - State "TODO" from [2016-08-22 Mon 20:56] 32 | :END: 33 | ...Where a new-line is inserted before not doing so would exceed the fill column to avoid situations like where multi-lining becomes very ineffective 34 | #+BEGIN_SRC emacs-lisp 35 | (org-projectile:update-project-to-org-filepath-fdsafd-fds "multi-line" 36 | (imalison:join-paths "~")) 37 | #+END_SRC 38 | 39 | The "aggresive" version would looke like 40 | 41 | #+BEGIN_SRC emacs-lisp 42 | (org-projectile:update-project-to-org-filepath-fdsafd-fds 43 | "multi-line" (imalison:join-paths "~")) 44 | #+END_SRC 45 | * TODO Recursive multi-lining 46 | :LOGBOOK: 47 | - State "TODO" from [2016-08-22 Mon 21:02] 48 | :END: 49 | Ex: 50 | #+BEGIN_SRC emacs-lisp 51 | (my-function-name "a long ass string" "more stuff" (afdasfdsa afdsafdsa afdsafdsa afdsafa afdfdsa "more crap" "crap")) 52 | #+END_SRC 53 | To: 54 | #+BEGIN_SRC emacs-lisp 55 | (my-function-name "a long ass string" "more stuff" 56 | (afdasfdsa afdsafdsa afdsafdsa afdsafa afdfdsa 57 | "more crap" "crap")) 58 | #+END_SRC 59 | Instead of: 60 | #+BEGIN_SRC emacs-lisp 61 | (my-function-name "a long ass string" "more stuff" 62 | (afdasfdsa afdsafdsa afdsafdsa afdsafa afdfdsa "more crap" "crap")) 63 | #+END_SRC 64 | * DONE Fill column does not seem to work properly 65 | :PROPERTIES: 66 | :CREATED: [2015-11-27 Fri 11:58] 67 | :END: 68 | :LOGBOOK: 69 | - State "DONE" from "TODO" [2016-08-24 Wed 16:23] 70 | - State "TODO" from [2015-11-27 Fri 11:58] 71 | :END: 72 | ...in situations where the next separator mark is not on the same line as the multi-lining line because it looks at the column number position at the separator (usually ,) 73 | * DONE Fill respacer does not consider characters from things that follow the end of the respaced definition 74 | :PROPERTIES: 75 | :CREATED: [2015-11-23 Mon 17:59] 76 | :END: 77 | :LOGBOOK: 78 | - State "DONE" from "TODO" [2016-08-24 Wed 16:24] 79 | - Note taken on [2015-11-30 Mon 20:24] \\ 80 | Here is an example: 81 | 82 | #+BEGIN_SRC go 83 | func (a *Account) paramsToStateChange(newState ordert.AccountState) *db.AccountStateChange { 84 | } 85 | #+END_SRC 86 | - State "TODO" from [2015-11-23 Mon 17:59] 87 | :END: 88 | * TODO Being inside of quotes can mess things up in python mode 89 | :PROPERTIES: 90 | :CREATED: [2015-11-23 Mon 13:03] 91 | :END: 92 | :LOGBOOK: 93 | - State "TODO" from [2015-11-23 Mon 13:03] 94 | :END: 95 | * DONE ruby-map literals 96 | :PROPERTIES: 97 | :CREATED: [2015-11-29 Sun 23:59] 98 | :END: 99 | :LOGBOOK: 100 | - State "DONE" from "TODO" [2016-08-26 Fri 23:56] 101 | - State "TODO" from [2015-11-29 Sun 23:59] 102 | :END: 103 | * TODO Apply hooks to existing buffers to make the right strategies active 104 | :LOGBOOK: 105 | - State "TODO" from "TODO" [2016-08-27 Sat 12:31] 106 | :END: 107 | * TODO Pairing on : keywords is too aggressive, what if the :keyword is actually an argument 108 | :LOGBOOK: 109 | - State "TODO" from [2016-08-27 Sat 12:31] 110 | :END: 111 | -------------------------------------------------------------------------------- /multi-line-candidate.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-candidate.el --- multi-line statements -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; multi-line-candidate defines a wrapper object for a marker with some 21 | ;; additional context 22 | 23 | ;;; Code: 24 | 25 | (require 'cl-lib) 26 | (require 'eieio) 27 | 28 | (defclass multi-line-candidate () 29 | ((marker :initarg :marker :initform (point-marker)) 30 | (original-spacing :initarg :original-spacing :initform nil))) 31 | 32 | (cl-defmethod multi-line-candidate-position ((candidate multi-line-candidate)) 33 | (marker-position (oref candidate marker))) 34 | 35 | (provide 'multi-line-candidate) 36 | ;;; multi-line-candidate.el ends here 37 | -------------------------------------------------------------------------------- /multi-line-cycle.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-cycle.el --- multi-line statements -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; multi-line-cycle defines a respacer object that dispatch to various 21 | ;; other respacers based on context and the previous user action. 22 | 23 | ;;; Code: 24 | 25 | (require 'cl-lib) 26 | (require 'eieio) 27 | 28 | (require 'multi-line-candidate) 29 | 30 | ;; This variable is for internal use only 31 | (defvar multi-line-last-cycler nil) 32 | 33 | (defclass multi-line-cycle-respacer () 34 | ((respacers :initarg :respacers) 35 | (named-respacers :initarg :named-respacers :initform nil) 36 | (last-cycle-marker :initform nil) 37 | (cycle-index :initform 0) 38 | (command-at-last-cycle :initform nil) 39 | (check-markers :initform t :initarg check-markers) 40 | (check-last-command :initform nil :initarg check-last-command))) 41 | 42 | (cl-defmethod multi-line-respace ((cycler multi-line-cycle-respacer) candidates 43 | &optional context) 44 | (let* ((respacer-name (plist-get context :respacer-name)) 45 | (respacer-index (plist-get context :respacer-index)) 46 | (respacer 47 | (or (plist-get (oref cycler named-respacers) 48 | respacer-name) 49 | (when respacer-index 50 | (nth respacer-index (oref cycler respacers)))))) 51 | (if respacer 52 | ;; We want to reset in this case because something has been selected 53 | ;; explicitly. 54 | (multi-line-cycler-reset cycler nil) 55 | ;; Pass in the first marker that was found so that we get consistent 56 | ;; cycling behavior even if the cursor is moved. 57 | (setq respacer (multi-line-cycle cycler (oref (car candidates) marker)))) 58 | (multi-line-respace respacer candidates context))) 59 | 60 | (cl-defmethod multi-line-cycle ((cycler multi-line-cycle-respacer) current-marker) 61 | (if (and (eq multi-line-last-cycler cycler) 62 | (or (not (oref cycler check-last-command)) 63 | (equal (oref cycler command-at-last-cycle) last-command)) 64 | (or (not (oref cycler check-markers)) 65 | (let ((last-marker (oref cycler last-cycle-marker))) 66 | (equal current-marker last-marker)))) 67 | (multi-line-increment-cycle-index cycler) 68 | (multi-line-cycler-reset cycler current-marker)) 69 | (multi-line-current-respacer cycler)) 70 | 71 | (cl-defmethod multi-line-current-respacer ((cycler multi-line-cycle-respacer)) 72 | (nth (oref cycler cycle-index) (oref cycler respacers))) 73 | 74 | (cl-defmethod multi-line-cycler-reset ((cycler multi-line-cycle-respacer) current-marker) 75 | (oset cycler last-cycle-marker current-marker) 76 | (oset cycler cycle-index 0) 77 | (oset cycler command-at-last-cycle this-command) 78 | (setq multi-line-last-cycler cycler)) 79 | 80 | (cl-defmethod multi-line-increment-cycle-index ((cycler multi-line-cycle-respacer) 81 | &optional amount) 82 | (unless amount (setq amount 1)) 83 | (oset cycler cycle-index 84 | (% (+ (oref cycler cycle-index) amount) 85 | (length (oref cycler respacers))))) 86 | 87 | (defun multi-line-build-from-respacers-list (respacers-list) 88 | (let* ((named-respacers nil) 89 | (respacers 90 | (cl-loop for respacer-spec in respacers-list 91 | collect (pcase respacer-spec 92 | (`(,name . ,respacer) 93 | (setq named-respacers 94 | (plist-put named-respacers name respacer)) 95 | respacer) 96 | (_ respacer-spec))))) 97 | (make-instance 'multi-line-cycle-respacer :respacers respacers 98 | :named-respacers named-respacers))) 99 | 100 | (provide 'multi-line-cycle) 101 | ;;; multi-line-cycle.el ends here 102 | -------------------------------------------------------------------------------- /multi-line-decorator.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-decorator.el --- multi-line statements -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; multi-line-decorator defines a collection of decorator respacers 21 | ;; that can be used to add behavior to existing respacers. 22 | 23 | ;;; Code: 24 | 25 | (require 'cl-lib) 26 | (require 'eieio) 27 | (require 'shut-up) 28 | 29 | (require 'multi-line-candidate) 30 | (require 'multi-line-respace) 31 | (require 'multi-line-shared) 32 | 33 | (put 'multi-line-pre-decorator 'lisp-indent-function 'defun) 34 | (put 'multi-line-post-decorator 'lisp-indent-function 'defun) 35 | (put 'multi-line-post-all-decorator 'lisp-indent-function 'defun) 36 | 37 | (defmacro multi-line-compose (name &rest funcs) 38 | "Build a new function with NAME that is the composition of FUNCS." 39 | `(defun ,name (arg) 40 | (multi-line-compose-helper ,funcs))) 41 | 42 | (defmacro multi-line-compose-helper (funcs) 43 | "Builds funcalls of FUNCS applied to the arg." 44 | (if (equal (length funcs) 0) 45 | (quote arg) 46 | `(funcall ,(car funcs) (multi-line-compose-helper ,(cdr funcs))))) 47 | 48 | ;; TODO: Get rid of inheritance here 49 | (defclass multi-line-each-decorator (multi-line-respacer) 50 | ((respacer :initarg :respacer) 51 | (decorator :initarg :decorator))) 52 | 53 | (cl-defmethod multi-line-respace-one ((decorator multi-line-each-decorator) 54 | index candidates) 55 | (funcall (oref decorator decorator) (oref decorator respacer) index 56 | candidates)) 57 | 58 | (defmacro multi-line-pre-decorator (name &rest forms) 59 | "Build respacer decorator NAME that execute FORMS before each respacing. 60 | 61 | FORMS can use the variables index and candidates which will be 62 | appropriately populated by the executor." 63 | `(defun ,name (decorated-respacer) 64 | (make-instance 'multi-line-each-decorator 65 | :respacer decorated-respacer 66 | :decorator (lambda (respacer index candidates) 67 | ,@forms 68 | (multi-line-respace-one respacer index candidates))))) 69 | 70 | (defmacro multi-line-post-decorator (name &rest forms) 71 | "Build respacer decorator NAME that execute FORMS after each respacing. 72 | 73 | FORMS can use the variables index and candidates which will be 74 | appropriately populated by the executor." 75 | `(defun ,name (respacer) 76 | (make-instance 'multi-line-each-decorator 77 | :respacer respacer 78 | :decorator (lambda (respacer index candidates) 79 | (multi-line-respace-one respacer index candidates) 80 | ,@forms)))) 81 | 82 | (defmacro multi-line-post-all-decorator (name &rest forms) 83 | "Build respacer decorator NAME that execute FORMS after all respacing. 84 | 85 | FORMS can use the variables index and candidates which will be 86 | appropriately populated by the executor." 87 | `(multi-line-post-decorator 88 | ,name (when (equal index (- (length candidates) 1)) 89 | (goto-char (multi-line-candidate-position (car (last candidates)))) 90 | ,@forms))) 91 | 92 | (multi-line-pre-decorator multi-line-space-clearing-respacer 93 | (multi-line-clear-whitespace-at-point)) 94 | 95 | (multi-line-post-all-decorator multi-line-trailing-comma-respacer 96 | (multi-line-add-remove-or-leave-final-comma)) 97 | 98 | (multi-line-post-decorator 99 | multi-line-reindenting-respacer 100 | (shut-up 101 | (indent-region (multi-line-candidate-position (car candidates)) 102 | (multi-line-candidate-position (car (last candidates)))))) 103 | 104 | (multi-line-post-decorator 105 | multi-line-per-line-reindenting-respacer 106 | (shut-up 107 | (let ((start-position (multi-line-candidate-position (car candidates))) 108 | (end-position (multi-line-candidate-position (car (last candidates)))) 109 | last-reindent) 110 | (save-excursion 111 | (goto-char start-position) 112 | (while (and (<= (point) end-position) (or (not last-reindent) (> (point) last-reindent))) 113 | (setq last-reindent (point)) 114 | (funcall indent-line-function) 115 | (forward-line) 116 | (move-beginning-of-line nil)))))) 117 | 118 | (multi-line-compose multi-line-clearing-reindenting-respacer 119 | 'multi-line-reindenting-respacer 120 | 'multi-line-space-clearing-respacer) 121 | 122 | (defclass multi-line-space-restoring-respacer () 123 | ((respacer :initarg :respacer))) 124 | 125 | (cl-defmethod multi-line-respace-one ((respacer multi-line-space-restoring-respacer) 126 | index candidates) 127 | (cl-destructuring-bind (startm . endm) (multi-line-space-markers) 128 | (let* ((start (marker-position startm)) 129 | (end (marker-position endm)) 130 | (spanning-start (progn 131 | (goto-char (- start 1)) 132 | (point-marker))) 133 | (spanning-end (progn 134 | (goto-char (+ end 1)) 135 | (point-marker))) 136 | (space-to-restore (buffer-substring start end))) 137 | (delete-region start end) 138 | (let ((spanning-string (buffer-substring (marker-position spanning-start) 139 | (marker-position spanning-end)))) 140 | (multi-line-respace-one (oref respacer respacer) index candidates) 141 | (when (equal (buffer-substring (marker-position spanning-start) 142 | (marker-position spanning-end)) 143 | spanning-string) 144 | (goto-char (marker-position startm)) 145 | (insert space-to-restore)))))) 146 | 147 | (defun multi-line-restoring-reindenting-respacer (respacer) 148 | (multi-line-reindenting-respacer 149 | (multi-line-space-restoring-respacer :respacer respacer))) 150 | 151 | (provide 'multi-line-decorator) 152 | ;;; multi-line-decorator.el ends here 153 | -------------------------------------------------------------------------------- /multi-line-enter.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-enter.el --- multi-line statements -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; multi-line-enter defines various approaches for going to the 21 | ;; beginning of a statement that can be multi-lined. 22 | 23 | ;;; Code: 24 | 25 | (require 'cl-lib) 26 | (require 'eieio) 27 | (require 'multi-line-shared) 28 | 29 | (defclass multi-line-up-list-enter-strategy () 30 | ((skip-chars :initarg :skip-chars :initform nil))) 31 | 32 | (cl-defmethod multi-line-enter ((enter multi-line-up-list-enter-strategy) 33 | &optional _context) 34 | (multi-line-up-list-back) 35 | (when (oref enter skip-chars) 36 | (while (looking-at (format "[%s]" (oref enter skip-chars))) 37 | (forward-char))) 38 | (forward-char)) 39 | 40 | (defclass multi-line-looking-at-enter-strategy () 41 | ((enter-regex :initarg :enter-regex :initform "[[:space]]*[{([]"))) 42 | 43 | (cl-defmethod multi-line-can-enter ((strategy multi-line-looking-at-enter-strategy)) 44 | (looking-at (oref strategy enter-regex))) 45 | 46 | (cl-defmethod multi-line-enter ((strategy multi-line-looking-at-enter-strategy)) 47 | (re-search-forward (oref strategy enter-regex))) 48 | 49 | (provide 'multi-line-enter) 50 | ;;; multi-line-enter.el ends here 51 | -------------------------------------------------------------------------------- /multi-line-find.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-find.el --- multi-line statements -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; multi-line-find defines various approaches for finding the points 21 | ;; at which a statement can be split into multiple lines. 22 | 23 | ;;; Code: 24 | 25 | (require 'cl-lib) 26 | (require 'eieio) 27 | 28 | (require 'multi-line-candidate) 29 | (require 'multi-line-enter) 30 | (require 'multi-line-shared) 31 | 32 | (defclass multi-line-forward-sexp-find-strategy () 33 | ((split-regex :initarg :split-regex :initform "[[:space:]]*,") 34 | (done-regex :initarg :done-regex :initform "[[:space:]]*[})\]]") 35 | (split-advance-fn :initarg :split-advance-fn :initform 36 | 'multi-line-comma-advance))) 37 | 38 | (cl-defmethod multi-line-at-end-of-candidates 39 | ((strategy multi-line-forward-sexp-find-strategy)) 40 | (or (looking-at (oref strategy done-regex)) 41 | (save-excursion 42 | ;; Check to see if we get a scan error when trying to move forward 43 | (condition-case _ignored (forward-sexp) ('scan-error t))))) 44 | 45 | (cl-defmethod multi-line-advance 46 | ((strategy multi-line-forward-sexp-find-strategy) &optional _context) 47 | (cl-loop 48 | for failed = (condition-case _ignored (forward-sexp) 49 | ('error t)) 50 | when (or failed 51 | (when (looking-at (oref strategy split-regex)) 52 | (funcall (oref strategy split-advance-fn)) 53 | t)) 54 | return nil)) 55 | 56 | (cl-defmethod multi-line-find ((strategy multi-line-forward-sexp-find-strategy) 57 | &optional context) 58 | (nconc (list (make-instance 'multi-line-candidate)) 59 | (progn 60 | ;; XXX: This is a hack to make hash literals work in ruby. For some 61 | ;; reason if you execute forward sexp at a '{' but there is a newline 62 | ;; immediately following that character it passes over the entire 63 | ;; hash body. 64 | (re-search-forward "[^[:space:]\n]") (backward-char) 65 | (cl-loop 66 | do (multi-line-advance strategy context) 67 | ;; Make sure we're not adding the same point twice 68 | until (and last-point (equal (point) last-point)) 69 | for last-point = (point) 70 | collect (make-instance 'multi-line-candidate) 71 | until (multi-line-at-end-of-candidates strategy))))) 72 | 73 | ;; A finder decorator that removes candidates that follow "keyword" arguments, 74 | ;; so that things like: 75 | ;; :akey "avalue" 76 | ;; are always paired 77 | (defclass multi-line-keyword-pairing-finder () 78 | ((child :initarg :child) 79 | (keyword-string :initarg :keyword-string :initform ":"))) 80 | 81 | (cl-defmethod multi-line-find ((strategy multi-line-keyword-pairing-finder) 82 | &optional context) 83 | (let ((candidates (multi-line-find (oref strategy child) context)) 84 | last-was-included last-candidate) 85 | (cl-loop for candidate in candidates 86 | for include-this = 87 | (or 88 | (not last-was-included) 89 | (progn 90 | (goto-char (multi-line-candidate-position last-candidate)) 91 | (re-search-forward "[^[:space:]]") 92 | (backward-char) 93 | (not (looking-at (oref strategy keyword-string))))) 94 | do (progn 95 | (setq last-was-included include-this) 96 | (setq last-candidate candidate)) 97 | when include-this 98 | collect candidate))) 99 | 100 | (provide 'multi-line-find) 101 | ;;; multi-line-find.el ends here 102 | -------------------------------------------------------------------------------- /multi-line-highlight.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-highlight.el --- multi-line statements -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; Author: Ivan Malison 6 | ;; Keywords: multi line length whitespace programming tools 7 | ;; URL: https://github.com/IvanMalison/multi-line 8 | ;; Package-Requires: ((emacs "24") (s "1.9.0") (cl-lib "0.5") (dash "2.12.0") (shut-up "0.3.2")) 9 | ;; Version: 0.1.4 10 | 11 | ;; This program is free software; you can redistribute it and/or modify 12 | ;; it under the terms of the GNU General Public License as published by 13 | ;; the Free Software Foundation, either version 3 of the License, or 14 | ;; (at your option) any later version. 15 | 16 | ;; This program is distributed in the hope that it will be useful, 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | ;; GNU General Public License for more details. 20 | 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with this program. If not, see . 23 | 24 | ;;; Commentary: 25 | 26 | ;; Highlight respacing candidates. 27 | 28 | ;;; Code: 29 | 30 | (require 'multi-line) 31 | 32 | (defvar multi-line-overlays-to-remove nil) 33 | 34 | ;;;###autoload 35 | (defun multi-line-clear-highlights () 36 | "Remove any existing multi-line highlight overlays." 37 | (interactive) 38 | (cl-loop for overlay in multi-line-overlays-to-remove 39 | do (delete-overlay overlay))) 40 | 41 | ;;;###autoload 42 | (defun multi-line-highlight-current-candidates () 43 | "Highlight the positions at which multi-line will consider adding newlines." 44 | (interactive) 45 | (let ((candidates 46 | (save-excursion 47 | (multi-line-candidates multi-line-current-strategy)))) 48 | (cl-loop for candidate in candidates do 49 | (push (multi-line-highlight-candidate candidate) 50 | multi-line-overlays-to-remove)))) 51 | 52 | (defun multi-line-highlight-candidate (candidate) 53 | (let* ((position (multi-line-candidate-position candidate)) 54 | (overlay (make-overlay (1- position) position))) 55 | (overlay-put overlay 'face 'highlight) 56 | overlay)) 57 | 58 | (provide 'multi-line-highlight) 59 | ;;; multi-line-highlight.el ends here 60 | -------------------------------------------------------------------------------- /multi-line-respace.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-respace.el --- multi-line statements -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; multi-line-respace defines various generally applicable respace 21 | ;; strategies. 22 | 23 | ;;; Code: 24 | 25 | (require 'cl-lib) 26 | (require 'eieio) 27 | (require 'dash) 28 | (require 's) 29 | 30 | (require 'multi-line-candidate) 31 | (require 'multi-line-cycle) 32 | (require 'multi-line-shared) 33 | 34 | (defclass multi-line-respacer () nil) 35 | 36 | (cl-defmethod multi-line-respace ((respacer multi-line-respacer) candidates 37 | &optional _context) 38 | (cl-loop for candidate being the elements of candidates using (index i) do 39 | (goto-char (multi-line-candidate-position candidate)) 40 | (multi-line-respace-one respacer i candidates))) 41 | 42 | (defclass multi-line-space (multi-line-respacer) 43 | ((spacer :initarg :spacer :initform " "))) 44 | 45 | (cl-defmethod multi-line-respace-one ((respacer multi-line-space) 46 | _index _candidates) 47 | (when (not (multi-line-spacer-at-point respacer)) 48 | (insert (oref respacer spacer)))) 49 | 50 | (cl-defmethod multi-line-spacer-at-point ((respacer multi-line-space)) 51 | ;; TODO/XXX: This would cause problems with a spacer that was more than one 52 | ;; character long. 53 | (save-excursion (re-search-backward (format "[^%s]" (oref respacer spacer))) 54 | (forward-char) 55 | (looking-at (oref respacer spacer)))) 56 | 57 | (defclass multi-line-always-newline (multi-line-respacer) nil) 58 | 59 | (cl-defmethod multi-line-respace-one ((_respacer multi-line-always-newline) 60 | _index _candidates) 61 | (newline-and-indent)) 62 | 63 | (defclass multi-line-selecting-respacer nil 64 | ((indices-to-respacer :initarg :indices-to-respacer) 65 | (default :initarg :default :initform nil))) 66 | 67 | (cl-defmethod multi-line-respace-one ((respacer multi-line-selecting-respacer) 68 | index candidates) 69 | (let ((selected (multi-line-select-respacer respacer index candidates))) 70 | (when selected 71 | (multi-line-respace-one selected index candidates)))) 72 | 73 | (cl-defmethod multi-line-select-respacer ((respacer multi-line-selecting-respacer) 74 | index candidates) 75 | (cl-loop for (indices . r) in (oref respacer indices-to-respacer) 76 | when 77 | (memq index (multi-line-actual-indices indices candidates)) 78 | return r 79 | finally return (oref respacer default))) 80 | 81 | (defun multi-line-never-newline () 82 | (make-instance 'multi-line-selecting-respacer 83 | :default (make-instance 'multi-line-space) 84 | :indices-to-respacer (list (cons (list 0 -1) nil)))) 85 | 86 | (defclass multi-line-fill-respacer (multi-line-respacer) 87 | ((newline-respacer 88 | :initarg :newline-respacer) 89 | (sl-respacer 90 | :initarg :sl-respacer) 91 | (first-index :initform 0 :initarg :first-index) 92 | (final-index :initform -1 :initarg :final-index))) 93 | 94 | ;; There seems to be a really strange issue where the default value for a field 95 | ;; passed to :initform is evaluated incorretly in SUBCLASSES. There may be some 96 | ;; interaction with byte compilation as well, causing this (though I haven't 97 | ;; verified this). 98 | ;; Specifically, when creating an instance of a subclass, instead of evaluating the 99 | ;; function call 'multi-line-never-newline' and using its return value to initialize 100 | ;; the 'sl-respacer' slot, it was treating 'multi-line-never-newline' as a literal 101 | ;; list containing a single symbol, and using that list as the default value. This 102 | ;; resulted in a type error later on when 'multi-line-respace-one' expected an 103 | ;; instance of 'multi-line-selecting-respacer', but instead received a list. 104 | (cl-defmethod initialize-instance :after ((obj multi-line-fill-respacer) &rest _args) 105 | (unless (slot-boundp obj 'newline-respacer) 106 | (oset obj newline-respacer (make-instance 'multi-line-always-newline))) 107 | (unless (slot-boundp obj 'sl-respacer) 108 | (oset obj sl-respacer (multi-line-never-newline)))) 109 | 110 | (cl-defmethod multi-line-should-newline ((respacer multi-line-fill-respacer) 111 | index candidates) 112 | (let ((candidates-length (length candidates))) 113 | (when (<= (multi-line-first-index respacer candidates-length) 114 | index (multi-line-final-index respacer candidates-length)) 115 | (multi-line-check-fill-column respacer index candidates)))) 116 | 117 | (cl-defmethod multi-line-first-index ((respacer multi-line-fill-respacer) 118 | candidates-length) 119 | (mod (oref respacer first-index) candidates-length)) 120 | 121 | (cl-defmethod multi-line-final-index ((respacer multi-line-fill-respacer) 122 | candidates-length) 123 | (mod (oref respacer final-index) candidates-length)) 124 | 125 | (cl-defmethod multi-line-check-fill-column ((respacer multi-line-fill-respacer) 126 | index candidates) 127 | (> (multi-line-min-max-column-if-no-newline respacer index candidates) 128 | (multi-line-get-fill-column respacer))) 129 | 130 | (cl-defmethod multi-line-min-max-column-if-no-newline 131 | ((respacer multi-line-fill-respacer) index candidates) 132 | "Compute the minimum line length of the current expression, 133 | assuming that no newline is inserted at the current candidate." 134 | 135 | (let* ((next-index (+ index 1)) 136 | (final-index (multi-line-final-index respacer (length candidates))) 137 | (next-candidate (nth next-index candidates)) 138 | (next-candidate-position 139 | (if next-candidate 140 | (multi-line-candidate-position next-candidate) 141 | (save-excursion (end-of-line) (point))))) 142 | (save-excursion 143 | (cond ((or 144 | ;; This is the last chance to respace, so we need to 145 | ;; consider anything else that is on the current line. 146 | (equal index final-index) 147 | ;; There is at least one newline in between this marker and the 148 | ;; next marker, so we maximize the end of line columns between 149 | ;; them. 150 | (s-contains? 151 | "\n" (buffer-substring (multi-line-candidate-position 152 | (nth index candidates)) 153 | next-candidate-position))) 154 | (cl-loop 155 | do (let ((inhibit-point-motion-hooks t)) 156 | (end-of-line)) 157 | maximize (current-column) 158 | do (forward-line) 159 | while (< (point) next-candidate-position))) 160 | (;; We look at the column of the next candidate 161 | next-candidate 162 | (goto-char (multi-line-candidate-position next-candidate)) 163 | (current-column)))))) 164 | 165 | (cl-defmethod multi-line-respace-one ((respacer multi-line-fill-respacer) 166 | index candidates) 167 | (let ((selected 168 | (if (multi-line-should-newline respacer index candidates) 169 | (oref respacer newline-respacer) 170 | (oref respacer sl-respacer)))) 171 | (multi-line-respace-one selected index candidates))) 172 | 173 | (defclass multi-line-fixed-fill-respacer (multi-line-fill-respacer) 174 | ((newline-at :initarg :newline-at :initform 80))) 175 | 176 | (cl-defmethod multi-line-get-fill-column 177 | ((respacer multi-line-fixed-fill-respacer)) 178 | (oref respacer newline-at)) 179 | 180 | (defclass multi-line-fill-column-respacer (multi-line-fill-respacer) nil) 181 | 182 | (cl-defmethod multi-line-get-fill-column ((_r multi-line-fill-column-respacer)) 183 | fill-column) 184 | 185 | (provide 'multi-line-respace) 186 | ;;; multi-line-respace.el ends here 187 | -------------------------------------------------------------------------------- /multi-line-shared.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-shared.el --- multi-line statements -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; multi-line-shared defines functions that are generally useful in 21 | ;; building multi-line strategies. 22 | 23 | ;;; Code: 24 | 25 | (require 'cl-lib) 26 | (require 'eieio) 27 | (require 'dash) 28 | (require 's) 29 | 30 | (defun multi-line-clear-whitespace-at-point () 31 | "Clear the whitespace at point." 32 | (interactive) 33 | (cl-destructuring-bind (start . end) 34 | (multi-line-space-markers) 35 | (delete-region (marker-position start) (marker-position end)))) 36 | 37 | (cl-defun multi-line-space-markers 38 | (&optional (space-matches-string "[:space:]\n")) 39 | "Get markers delimiting whitespace at point. 40 | 41 | SPACE-MATCHES-STRING is as a string containing concatenated 42 | character classes that will be used to find whitespace." 43 | (let ((space-excludes-string (format "[^%s]" space-matches-string))) 44 | (re-search-backward space-excludes-string) 45 | (forward-char) 46 | (let* ((start (point-marker)) 47 | (end (progn 48 | (re-search-forward space-excludes-string) 49 | (backward-char) 50 | (point-marker)))) 51 | (cons start end)))) 52 | 53 | (defun multi-line-add-remove-or-leave-final-comma () 54 | (save-excursion 55 | (if (looking-at ",?[[:space:]]*\n") 56 | (when (not (or (looking-at ",") 57 | (save-excursion 58 | (re-search-backward "[^[:space:]\n]") 59 | (looking-at ",")))) 60 | (insert ",")) 61 | (when (or (looking-at ",") 62 | (progn (re-search-backward "[^[:space:]\n]") 63 | (looking-at ","))) 64 | (delete-char 1))))) 65 | 66 | (defun multi-line-lparenthesis-advance () 67 | "Advance to the beginning of a statement that can be multi-lined." 68 | (re-search-forward "[[{(]")) 69 | 70 | (defun multi-line-up-list-back () 71 | "Go to the beginning of a statement from inside the statement." 72 | (interactive) 73 | ;; TODO: This could really used some explanation. I have no idea what is going 74 | ;; on here now. 75 | (let ((string-start (nth 8 (syntax-ppss)))) 76 | (when string-start 77 | (goto-char string-start))) 78 | (up-list) (backward-sexp)) 79 | 80 | (defun multi-line-comma-advance () 81 | "Pass over a comma when it is present." 82 | (when (looking-at "[[:space:]\n]*,") 83 | (re-search-forward ","))) 84 | 85 | (defun multi-line-is-newline-between-markers (first second) 86 | (s-contains? "\n" 87 | (buffer-substring (marker-position first) 88 | (marker-position second)))) 89 | 90 | (defmacro multi-line-predicate-or (&rest predicates) 91 | `(lambda (&rest args) 92 | (or ,@(cl-loop for predicate in predicates 93 | collect `(apply ,predicate args))))) 94 | 95 | (defmacro multi-line-predicate-and (&rest predicates) 96 | `(lambda (&rest args) 97 | (and ,@(cl-loop for predicate in predicates 98 | collect `(apply ,predicate args))))) 99 | 100 | (defun multi-line-last-predicate (index candidates) 101 | (equal index (- (length candidates) 1))) 102 | 103 | (defun multi-line-first-predicate (index _candidates) 104 | (equal index 0)) 105 | 106 | (defalias 'multi-line-first-or-last-predicate 107 | (multi-line-predicate-or 'multi-line-first-predicate 108 | 'multi-line-last-predicate)) 109 | 110 | (defun multi-line-remove-at-indices (skip-indices list) 111 | (-remove-at-indices (multi-line-actual-indices skip-indices list) list)) 112 | 113 | (defun multi-line-actual-indices (skip-indices list) 114 | (let ((list-length (length list))) 115 | (cl-loop for index in skip-indices 116 | collect (mod index list-length)))) 117 | 118 | (defun multi-line-interpret-prefix-as-number (prefix) 119 | (cond 120 | ((numberp prefix) prefix) 121 | ((and (-non-nil prefix) (listp prefix)) 122 | (truncate (log (car prefix) 4))) 123 | (0))) 124 | 125 | (provide 'multi-line-shared) 126 | ;;; multi-line-shared.el ends here 127 | -------------------------------------------------------------------------------- /multi-line.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line.el --- multi-line statements -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; Author: Ivan Malison 6 | ;; Keywords: multi line length whitespace programming tools convenience files 7 | ;; URL: https://github.com/IvanMalison/multi-line 8 | ;; Package-Requires: ((emacs "24.3") (s "1.9.0") (cl-lib "0.5") (dash "2.12.0") (shut-up "0.3.2")) 9 | ;; Version: 0.1.5 10 | 11 | ;; This program is free software; you can redistribute it and/or modify 12 | ;; it under the terms of the GNU General Public License as published by 13 | ;; the Free Software Foundation, either version 3 of the License, or 14 | ;; (at your option) any later version. 15 | 16 | ;; This program is distributed in the hope that it will be useful, 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | ;; GNU General Public License for more details. 20 | 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with this program. If not, see . 23 | 24 | ;;; Commentary: 25 | 26 | ;; multi-line aims to provide a flexible framework for automatically 27 | ;; multi-lining and single-lining function invocations and definitions, 28 | ;; array and map literals and more. It relies on functions that are 29 | ;; defined on a per major mode basis wherever it can so that it operates 30 | ;; correctly across many different programming languages. 31 | 32 | ;;; Code: 33 | 34 | (require 'cl-lib) 35 | (require 'eieio) 36 | 37 | (require 'multi-line-cycle) 38 | (require 'multi-line-decorator) 39 | (require 'multi-line-enter) 40 | (require 'multi-line-find) 41 | (require 'multi-line-respace) 42 | (require 'multi-line-shared) 43 | 44 | (defvar multi-line-default-single-line-respacer 45 | (multi-line-clearing-reindenting-respacer 46 | (multi-line-never-newline))) 47 | 48 | (defvar multi-line-always-newline-respacer 49 | (make-instance 'multi-line-always-newline)) 50 | 51 | (defvar multi-line-force-first-and-last-respacer 52 | (make-instance 53 | 'multi-line-selecting-respacer 54 | :indices-to-respacer (list 55 | (cons (list 0 -1) 56 | multi-line-always-newline-respacer)) 57 | :default (make-instance 'multi-line-fill-column-respacer))) 58 | 59 | (defvar multi-line-skip-first-and-last-respacer 60 | (make-instance 'multi-line-fill-column-respacer 61 | :first-index 1 :final-index -2)) 62 | 63 | (cl-defun multi-line-respacers-with-single-line 64 | (respacers 65 | &optional (single-line-respacer multi-line-default-single-line-respacer)) 66 | (multi-line-build-from-respacers-list 67 | (append respacers (list (cons :single-line single-line-respacer))))) 68 | 69 | (defun multi-line-default-respacers (&rest respacers) 70 | "Add a single-line strategy to RESPACERS and make a cycling respace strategy." 71 | (multi-line-respacers-with-single-line respacers)) 72 | 73 | (defvar multi-line-skip-fill-respacer 74 | (make-instance 'multi-line-fill-column-respacer 75 | :first-index 1 :final-index -2)) 76 | 77 | (defvar multi-line-default-respacer-list 78 | (mapcar 'multi-line-clearing-reindenting-respacer 79 | (list multi-line-force-first-and-last-respacer 80 | multi-line-always-newline-respacer 81 | multi-line-skip-first-and-last-respacer))) 82 | 83 | (defvar multi-line-default-respacer 84 | (multi-line-respacers-with-single-line multi-line-default-respacer-list)) 85 | 86 | (defun multi-line-get-default-respacer () 87 | multi-line-default-respacer) 88 | 89 | (defclass multi-line-strategy () 90 | ((enter :initarg :enter :initform 91 | (make-instance multi-line-up-list-enter-strategy)) 92 | (find :initarg :find :initform 93 | (make-instance multi-line-forward-sexp-find-strategy)) 94 | (respace :initarg :respace 95 | :initform (multi-line-get-default-respacer)))) 96 | 97 | (cl-defmethod multi-line-candidates ((strategy multi-line-strategy) 98 | &optional context) 99 | "Get the multi-line candidates at point using the find attribute of STRATEGY. 100 | CONTEXT is passed to the find strategy." 101 | (let ((enter-strategy (oref strategy enter)) 102 | (find-strategy (oref strategy find))) 103 | (multi-line-enter enter-strategy context) 104 | (multi-line-find find-strategy context))) 105 | 106 | (cl-defmethod multi-line-execute ((strategy multi-line-strategy) &optional context) 107 | (when (or (eq context t) (equal context 'single-line)) 108 | (setq context (plist-put nil :respacer-name :single-line))) 109 | (save-excursion 110 | (let ((candidates (multi-line-candidates strategy))) 111 | (multi-line-respace (oref strategy respace) candidates context)))) 112 | 113 | (defvar-local multi-line-current-strategy 114 | (make-instance 'multi-line-strategy) 115 | "The multi-line strategy that will be used by the command `multi-line'.") 116 | 117 | (defun multi-line-lisp-advance-fn () 118 | "Advance to the start of the next multi-line split for Lisp." 119 | nil) 120 | 121 | (eval-and-compile 122 | (defvar multi-line-defhook-prefix "multi-line-")) 123 | 124 | (defvar multi-line-mode-to-hook nil) 125 | 126 | (defmacro multi-line-defhook 127 | (the-mode-name strategy-form &optional use-global-enable) 128 | (let* ((mode-string (symbol-name the-mode-name)) 129 | (base-string (concat multi-line-defhook-prefix mode-string)) 130 | (variable-name (intern (concat base-string "-strategy"))) 131 | (hook-name (intern (concat base-string "-mode-hook"))) 132 | (mode-hook-name (intern (concat mode-string "-mode-hook")))) 133 | `(progn 134 | (defvar ,variable-name) 135 | (setq ,variable-name ,strategy-form) 136 | (defun ,hook-name () 137 | (setq-local multi-line-current-strategy ,variable-name)) 138 | ,(if use-global-enable 139 | `(add-to-list (quote multi-line-mode-to-hook) 140 | (cons (quote ,mode-hook-name) (quote ,hook-name))) 141 | `(add-hook (quote ,mode-hook-name) (quote ,hook-name) t))))) 142 | 143 | (put 'multi-line-defhook 'lisp-indent-function 1) 144 | 145 | (defvar multi-line-lisp-respacer 146 | (multi-line-default-respacers (multi-line-clearing-reindenting-respacer 147 | multi-line-skip-fill-respacer))) 148 | 149 | (defvar multi-line-lisp-find-strategy 150 | (make-instance 151 | 'multi-line-keyword-pairing-finder :child 152 | (make-instance 'multi-line-forward-sexp-find-strategy 153 | :split-regex "[[:space:]\n]+" 154 | :done-regex "[[:space:]]*)" 155 | :split-advance-fn 'multi-line-lisp-advance-fn))) 156 | 157 | (defvar multi-line-lisp-strategy 158 | (make-instance 159 | 'multi-line-strategy 160 | :find multi-line-lisp-find-strategy 161 | :enter (make-instance 'multi-line-up-list-enter-strategy 162 | :skip-chars "`',@") 163 | :respace multi-line-lisp-respacer)) 164 | 165 | (multi-line-defhook lisp multi-line-lisp-strategy t) 166 | (multi-line-defhook emacs-lisp multi-line-lisp-strategy t) 167 | 168 | (defvar multi-line-add-trailing-comma-strategy 169 | (make-instance 'multi-line-strategy 170 | :respace 171 | (multi-line-respacers-with-single-line 172 | (mapcar 'multi-line-trailing-comma-respacer 173 | multi-line-default-respacer-list) 174 | (multi-line-trailing-comma-respacer 175 | multi-line-default-single-line-respacer)))) 176 | 177 | (multi-line-defhook python multi-line-add-trailing-comma-strategy t) 178 | (multi-line-defhook go multi-line-add-trailing-comma-strategy t) 179 | (multi-line-defhook ruby multi-line-add-trailing-comma-strategy t) 180 | 181 | (defvar multi-line-leading-commas-find-strategy 182 | (make-instance 'multi-line-forward-sexp-find-strategy 183 | :split-advance-fn (lambda ()))) 184 | 185 | (defvar multi-line-leading-commas-strategy 186 | (make-instance 187 | 'multi-line-strategy 188 | :find multi-line-leading-commas-find-strategy 189 | :respace 190 | (multi-line-default-respacers 191 | (multi-line-clearing-reindenting-respacer 192 | (make-instance 'multi-line-always-newline))))) 193 | 194 | (multi-line-defhook haskell multi-line-leading-commas-strategy t) 195 | 196 | (defvar multi-line-clojure-find-strategy 197 | (make-instance 198 | 'multi-line-keyword-pairing-finder :child 199 | (make-instance 'multi-line-forward-sexp-find-strategy 200 | :split-regex "[[:space:]\n]+" 201 | :done-regex "[[:space:]]*)]}" 202 | :split-advance-fn 'multi-line-lisp-advance-fn))) 203 | 204 | (multi-line-defhook clojure 205 | (make-instance 206 | 'multi-line-strategy 207 | :find multi-line-clojure-find-strategy 208 | :enter (make-instance 'multi-line-up-list-enter-strategy 209 | :skip-chars "#~`'@,") 210 | :respace multi-line-lisp-respacer) t) 211 | 212 | ;;;###autoload 213 | (defun multi-line-enable-mode-hooks () 214 | "Set default language specific strategies for multi-line." 215 | (interactive) 216 | (cl-loop for (target-name . hook-name) in multi-line-mode-to-hook 217 | do (add-hook target-name hook-name t))) 218 | 219 | ;;;###autoload 220 | (defun multi-line-disable-mode-hooks () 221 | "Remove default language specific strategies for multi-line." 222 | (interactive) 223 | (cl-loop for (target-name . hook-name) in multi-line-mode-to-hook 224 | do (remove-hook target-name hook-name))) 225 | 226 | (multi-line-enable-mode-hooks) 227 | 228 | ;;;###autoload 229 | (defun multi-line (arg) 230 | "Multi-line the statement at point. 231 | 232 | When ARG is provided single-line the statement at point instead." 233 | (interactive "P") 234 | ;; TODO(imalison): Is there a better way to cast to bool? 235 | (let ((for-single-line (if arg t nil))) 236 | (multi-line-execute multi-line-current-strategy for-single-line))) 237 | 238 | ;;;###autoload 239 | (defun multi-line-single-line () 240 | "Single-line the statement at point." 241 | (interactive) 242 | (multi-line-execute multi-line-current-strategy t)) 243 | 244 | (provide 'multi-line) 245 | ;;; multi-line.el ends here 246 | -------------------------------------------------------------------------------- /test/multi-line-elisp-test.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-elisp-test.el --- multi-line elisp tests -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; Test the behavior of multi-line in emacs-lisp-mode 21 | 22 | ;;; Code: 23 | 24 | (require 'multi-line) 25 | (require 'multi-line-test) 26 | 27 | (cl-defmacro multi-line-deftest-elisp (name initial expected &rest args 28 | &key tags setup &allow-other-keys) 29 | (let ((setup (cons '(forward-char) setup))) 30 | `(multi-line-deftest-for-mode emacs-lisp ,name ,initial ,expected :tags (quote ,tags) 31 | :setup ,setup ,@args))) 32 | 33 | (multi-line-deftest-elisp test-basic-elisp 34 | "(a bbbbbbbbbbbbbbbbbb ccccccccccccccccccccc ddddddddeeeeeeeeekkkkkkkkkkffffffffff gggggggggggg)" 35 | "(a bbbbbbbbbbbbbbbbbb ccccccccccccccccccccc ddddddddeeeeeeeeekkkkkkkkkkffffffffff 36 | gggggggggggg)") 37 | 38 | (multi-line-deftest-elisp test-handles-quoted-lists 39 | "(list '((emacs-lisp-mode) (more-fun-stuff) (setq fill-column 80) (setq indent-tabs-mode nil) (forward-char)))" 40 | "(list '((emacs-lisp-mode) (more-fun-stuff) (setq fill-column 80) 41 | (setq indent-tabs-mode nil) (forward-char)))" 42 | :setup ((search-forward "(more-fun") 43 | (up-list))) 44 | 45 | (multi-line-deftest-elisp test-handles-quasi-quoted-lists 46 | "(list `((emacs-lisp-mode) (more-fun-stuff) (setq fill-column 80) (setq indent-tabs-mode nil) (forward-char)))" 47 | "(list `((emacs-lisp-mode) (more-fun-stuff) (setq fill-column 80) 48 | (setq indent-tabs-mode nil) (forward-char)))" 49 | :setup ((search-forward "(more-fun") 50 | (up-list))) 51 | 52 | (multi-line-deftest-elisp test-handles-unquote 53 | "`(,(list 'afdsafdsafdsfafdsafdsafd 'afdsafdsafdasfdsafs 'afdasfdsafdsafdafdsafdsafdsafdsa))" 54 | "`(,(list 'afdsafdsafdsfafdsafdsafd 'afdsafdsafdasfdsafs 55 | 'afdasfdsafdsafdafdsafdsafdsafdsa))" 56 | :setup ((search-forward "fdsafdsafdsfafdsafdsafd"))) 57 | 58 | (multi-line-deftest-elisp test-handles-splice 59 | "`(,@(list 'afdsafdsafdsfafdsafdsafd 'afdsafdsafdasfdsafs 'afdasfdsafdsafdafdsafdsafdsafdsa))" 60 | "`(,@(list 'afdsafdsafdsfafdsafdsafd 'afdsafdsafdasfdsafs 61 | 'afdasfdsafdsafdafdsafdsafdsafdsa))" 62 | :setup ((search-forward "fdsafdsafdsfafdsafdsafd"))) 63 | 64 | (multi-line-deftest-elisp colon-keywords-always-paired 65 | "(fdsafdsajklfdjsaklf fdasfdsafdsa fdasfdsafdsafdsafdsaf :f fdsafdsafdfdsafsafdsadf :c fdasfdsafdsafdsafdsafdsafdsafdsafdsafdsa)" 66 | "(fdsafdsajklfdjsaklf fdasfdsafdsa fdasfdsafdsafdsafdsaf 67 | :f fdsafdsafdfdsafsafdsadf 68 | :c fdasfdsafdsafdsafdsafdsafdsafdsafdsafdsa)") 69 | 70 | (multi-line-deftest-elisp test-checks-all-newlines-between-candidates 71 | "(fdsafkldsfdsafdsafdsaf fdsafdsafdsaf fdsafdsafdsa (afdasffda 72 | fdsafdsafdsaf fdsafdsaf fdsafdsafdsafdsa fdsafdsa) a)" 73 | "(fdsafkldsfdsafdsafdsaf fdsafdsafdsaf fdsafdsafdsa 74 | (afdasffda 75 | fdsafdsafdsaf fdsafdsaf fdsafdsafdsafdsa fdsafdsa) a)") 76 | 77 | (multi-line-deftest-elisp test-checks-newlines-if-last-candidate 78 | "(fdsafkldsfdsafdsafdsaf fdsafdsafdsaf fdsafdsafdsa (afdasffda 79 | fdsafdsafdsaf fdsafdsaf fdsafdsafdsafdsa fdsafdsa))" 80 | "(fdsafkldsfdsafdsafdsaf fdsafdsafdsaf fdsafdsafdsa 81 | (afdasffda 82 | fdsafdsafdsaf fdsafdsaf fdsafdsafdsafdsa fdsafdsa))") 83 | 84 | (multi-line-deftest-elisp test-find-index-multi-lining 85 | " 86 | (cl-defun tile-get-next-strategy 87 | (&optional (current-strategy (or tile-current-strategy 88 | (car (last tile-strategies))))) 89 | (let ((current-index (--find-index (equal current-strategy it) tile-strategies))) 90 | (if current-index 91 | (nth (mod (1+ current-index) (length tile-strategies)) tile-strategies) 92 | (car tile-strategies))))" 93 | " 94 | (cl-defun tile-get-next-strategy 95 | (&optional (current-strategy (or tile-current-strategy 96 | (car (last tile-strategies))))) 97 | (let ((current-index (--find-index (equal current-strategy it) 98 | tile-strategies))) 99 | (if current-index 100 | (nth (mod (1+ current-index) (length tile-strategies)) tile-strategies) 101 | (car tile-strategies))))" 102 | :setup ((search-forward "find-index"))) 103 | 104 | (provide 'multi-line-elisp-test) 105 | ;;; multi-line-elisp-test.el ends here 106 | -------------------------------------------------------------------------------- /test/multi-line-python-test.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-python-test.el --- multi-line python tests -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; Test the behavior of multi-line in python-mode 21 | 22 | ;;; Code: 23 | 24 | (require 'multi-line) 25 | (require 'multi-line-test) 26 | 27 | (multi-line-deftest-for-mode python test-basic-python 28 | " 29 | function(nested(fdasfdsaf, fdasfdsaf, fdasfdsaf, fdasfdsa), other, next, another_nested_call(more, cool, quite)) 30 | " 31 | (list 32 | " 33 | function( 34 | nested(fdasfdsaf, fdasfdsaf, fdasfdsaf, fdasfdsa), other, next, 35 | another_nested_call(more, cool, quite), 36 | ) 37 | " 38 | " 39 | function( 40 | nested(fdasfdsaf, fdasfdsaf, fdasfdsaf, fdasfdsa), 41 | other, 42 | next, 43 | another_nested_call(more, cool, quite), 44 | ) 45 | " 46 | " 47 | function(nested(fdasfdsaf, fdasfdsaf, fdasfdsaf, fdasfdsa), other, next, 48 | another_nested_call(more, cool, quite)) 49 | " 50 | " 51 | function(nested(fdasfdsaf, fdasfdsaf, fdasfdsaf, fdasfdsa), other, next, another_nested_call(more, cool, quite)) 52 | ") 53 | :setup ((search-forward "(") (forward-char))) 54 | 55 | (multi-line-deftest-for-mode python test-python-one-argument 56 | " 57 | fdsafdsafdsafdsafdsafdsafdsafdsafdsa(fdsafdsafdsafdsafdsafdsafdsafdfdsasdfdsadfdsaddffdsaf) 58 | " 59 | " 60 | fdsafdsafdsafdsafdsafdsafdsafdsafdsa( 61 | fdsafdsafdsafdsafdsafdsafdsafdfdsasdfdsadfdsaddffdsaf, 62 | ) 63 | " 64 | :setup ((search-forward "(") (forward-char))) 65 | 66 | (multi-line-deftest-for-mode python test-python-nested-dict 67 | " 68 | function(nested(fdasfdsaf, fdasfdsaf, fdasfdsaf, fdasfdsa), { 69 | 'a': 'bfdsafdsafdsafdsafdsafdsafdsafdsafdsafdsa', 70 | 'c': 'bfdsafdjksalf;djsakf;djsaklf;fdjksal;fdsa', 71 | }, next, another_nested_call(more, cool, quite)) 72 | " 73 | (list " 74 | function( 75 | nested(fdasfdsaf, fdasfdsaf, fdasfdsaf, fdasfdsa), { 76 | 'a': 'bfdsafdsafdsafdsafdsafdsafdsafdsafdsafdsa', 77 | 'c': 'bfdsafdjksalf;djsakf;djsaklf;fdjksal;fdsa', 78 | }, next, another_nested_call(more, cool, quite), 79 | ) 80 | " 81 | " 82 | function( 83 | nested(fdasfdsaf, fdasfdsaf, fdasfdsaf, fdasfdsa), 84 | { 85 | 'a': 'bfdsafdsafdsafdsafdsafdsafdsafdsafdsafdsa', 86 | 'c': 'bfdsafdjksalf;djsakf;djsaklf;fdjksal;fdsa', 87 | }, 88 | next, 89 | another_nested_call(more, cool, quite), 90 | ) 91 | " 92 | " 93 | function(nested(fdasfdsaf, fdasfdsaf, fdasfdsaf, fdasfdsa), { 94 | 'a': 'bfdsafdsafdsafdsafdsafdsafdsafdsafdsafdsa', 95 | 'c': 'bfdsafdjksalf;djsakf;djsaklf;fdjksal;fdsa', 96 | }, next, another_nested_call(more, cool, quite)) 97 | ") 98 | :setup ((search-forward "(") (forward-char))) 99 | 100 | (provide 'multi-line-python-test) 101 | ;;; multi-line-python-test.el ends here 102 | -------------------------------------------------------------------------------- /test/multi-line-ruby-test.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-ruby-test.el --- multi-line ruby tests -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; Test the behavior of multi-line in ruby-mode 21 | 22 | ;;; Code: 23 | 24 | (require 'multi-line) 25 | (require 'multi-line-test) 26 | 27 | (multi-line-deftest-for-mode ruby test-ruby-hash-literal 28 | "{:avalue => \"cool\", :greater => \"fun\", :avalue3 => \"cool\", :greaterg => \"fun\", :more => \"coool real long\",}" 29 | "{ 30 | :avalue => \"cool\", :greater => \"fun\", :avalue3 => \"cool\", :greaterg => \"fun\", 31 | :more => \"coool real long\", 32 | }" 33 | :setup ((search-forward "a"))) 34 | 35 | (provide 'multi-line-ruby-test) 36 | ;;; multi-line-ruby-test.el ends here 37 | -------------------------------------------------------------------------------- /test/multi-line-test.el: -------------------------------------------------------------------------------- 1 | ;;; multi-line-test.el --- multi-line test suite -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2015-2023 Ivan Malison 4 | 5 | ;; This program is free software; you can redistribute it and/or modify 6 | ;; it under the terms of the GNU General Public License as published by 7 | ;; the Free Software Foundation, either version 3 of the License, or 8 | ;; (at your option) any later version. 9 | 10 | ;; This program is distributed in the hope that it will be useful, 11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | ;; GNU General Public License for more details. 14 | 15 | ;; You should have received a copy of the GNU General Public License 16 | ;; along with this program. If not, see . 17 | 18 | ;;; Commentary: 19 | 20 | ;; The unit test suite of multi-line 21 | 22 | ;;; Code: 23 | 24 | (require 'cl-lib) 25 | (require 'eieio) 26 | (require 'ert) 27 | (require 'shut-up) 28 | 29 | (require 'multi-line) 30 | 31 | (put 'multi-line-deftest 'lisp-indent-function '(lambda (&rest args) 0)) 32 | 33 | (defun multi-line-whitespace-trim (original) 34 | (split-string original nil nil "[[:space:]]")) 35 | 36 | (cl-defmacro multi-line-deftest 37 | (name initial-text expected-text &key strategy tags setup) 38 | (let ((expected-texts (if (listp expected-text) 39 | expected-text 40 | `(list ,expected-text))) 41 | (test-name (intern (concat "multi-line-" (symbol-name name))))) 42 | `(ert-deftest ,test-name () 43 | :tags ,tags 44 | (with-temp-buffer 45 | (insert ,initial-text) 46 | ,(when strategy 47 | `(setq multi-line-current-strategy ,strategy)) 48 | (goto-char (point-min)) 49 | ,@setup 50 | (cl-loop for expected-text in ,expected-texts 51 | do (multi-line nil) 52 | (should 53 | (equal (multi-line-whitespace-trim expected-text) 54 | (multi-line-whitespace-trim (buffer-string)))))) 55 | t))) 56 | 57 | (put 'multi-line-deftest-for-mode 'lisp-indent-function '(lambda (&rest args) 0)) 58 | 59 | (cl-defmacro multi-line-deftest-for-mode 60 | (mode name initial expected &rest args &key tags setup &allow-other-keys) 61 | (let ((new-tags (cons mode tags)) 62 | (setup (cons `(progn 63 | (shut-up (,(intern (concat (symbol-name mode) "-mode")))) 64 | (setq fill-column 80 65 | indent-tabs-mode nil)) 66 | setup))) 67 | `(multi-line-deftest ,name ,initial ,expected :tags (quote ,new-tags) :setup ,setup ,@args))) 68 | 69 | (provide 'multi-line-test) 70 | ;;; multi-line-test.el ends here 71 | --------------------------------------------------------------------------------