├── .gitignore ├── .python-version ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── LICENSE ├── Main.sublime-menu ├── README.md ├── SmarterLineMoves.sublime-settings ├── messages.json ├── messages ├── 1.1.0.txt ├── 1.1.1.txt ├── 1.1.2.txt └── install.txt └── smarter_line_moves.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.todo 2 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | { "keys": ["ctrl+shift+up"], "command": "smart_swap_line_up" , "context": 4 | [ 5 | { "key": "slm_settings.smart_swap_up", "operator": "equal", "operand": true }, 6 | ] 7 | }, 8 | { "keys": ["ctrl+shift+down"], "command": "smart_swap_line_down" , "context": 9 | [ 10 | { "key": "slm_settings.smart_swap_down", "operator": "equal", "operand": true }, 11 | ] 12 | }, 13 | { "keys": ["ctrl+shift+up"], "command": "swap_line_above", "context": 14 | [ 15 | { "key": "slm_settings.swap_above", "operator": "equal", "operand": true }, 16 | { "key": "selection_in_first_line", "operator": "equal", "operand": true } 17 | ] 18 | }, 19 | { "keys": ["ctrl+shift+down"], "command": "swap_line_below", "context": 20 | [ 21 | { "key": "slm_settings.swap_below", "operator": "equal", "operand": true }, 22 | { "key": "selection_in_last_line", "operator": "equal", "operand": true } 23 | ] 24 | }, 25 | { "keys": ["ctrl+shift+down"], "command": "unswap_line_above", "context": 26 | [ 27 | { "key": "slm_settings.undo_swap_above", "operator": "equal", "operand": true }, 28 | { "key": "last_modifying_command", "operator": "equal", "operand" : "swap_line_above" }, 29 | { "key": "selection_in_first_line", "operator": "equal", "operand": true }, 30 | { "key": "selection_in_last_line", "operator": "equal", "operand": false } 31 | ] 32 | }, 33 | { "keys": ["ctrl+shift+down"], "command": "unswap_line_above", "context": 34 | [ 35 | { "key": "slm_settings.undo_swap_above", "operator": "equal", "operand": true }, 36 | { "key": "last_modifying_command", "operator": "equal", "operand" : "unswap_line_above" }, 37 | { "key": "selection_in_first_line", "operator": "equal", "operand": true }, 38 | { "key": "selection_in_last_line", "operator": "equal", "operand": false } 39 | ] 40 | }, 41 | { "keys": ["ctrl+shift+up"], "command": "unswap_line_below", "context": 42 | [ 43 | { "key": "slm_settings.undo_swap_below", "operator": "equal", "operand": true }, 44 | { "key": "last_modifying_command", "operator": "equal", "operand" : "swap_line_below" }, 45 | { "key": "selection_in_last_line", "operator": "equal", "operand": true }, 46 | { "key": "selection_in_first_line", "operator": "equal", "operand": false } 47 | ] 48 | }, 49 | { "keys": ["ctrl+shift+up"], "command": "unswap_line_below", "context": 50 | [ 51 | { "key": "slm_settings.undo_swap_below", "operator": "equal", "operand": true }, 52 | { "key": "last_modifying_command", "operator": "equal", "operand" : "unswap_line_below" }, 53 | { "key": "selection_in_last_line", "operator": "equal", "operand": true }, 54 | { "key": "selection_in_first_line", "operator": "equal", "operand": false }, 55 | ] 56 | }, 57 | { "keys": ["ctrl+alt+shift+up"], "command": "separate_text_up" }, 58 | { "keys": ["ctrl+alt+shift+down"], "command": "separate_text_down" }, 59 | { "keys": ["ctrl+alt+shift+right"], "command": "repel_text" }, 60 | { "keys": ["ctrl+alt+shift+left"], "command": "attract_text" }, 61 | 62 | ] 63 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | { "keys": ["ctrl+super+up"], "command": "smart_swap_line_up" , "context": 4 | [ 5 | { "key": "slm_settings.smart_swap_up", "operator": "equal", "operand": true }, 6 | ] 7 | }, 8 | { "keys": ["ctrl+super+down"], "command": "smart_swap_line_down" , "context": 9 | [ 10 | { "key": "slm_settings.smart_swap_down", "operator": "equal", "operand": true }, 11 | ] 12 | }, 13 | { "keys": ["ctrl+super+up"], "command": "swap_line_above", "context": 14 | [ 15 | { "key": "slm_settings.swap_above", "operator": "equal", "operand": true }, 16 | { "key": "selection_in_first_line", "operator": "equal", "operand": true } 17 | ] 18 | }, 19 | { "keys": ["ctrl+super+down"], "command": "swap_line_below", "context": 20 | [ 21 | { "key": "slm_settings.swap_below", "operator": "equal", "operand": true }, 22 | { "key": "selection_in_last_line", "operator": "equal", "operand": true } 23 | ] 24 | }, 25 | { "keys": ["ctrl+super+down"], "command": "unswap_line_above", "context": 26 | [ 27 | { "key": "slm_settings.undo_swap_above", "operator": "equal", "operand": true }, 28 | { "key": "last_modifying_command", "operator": "equal", "operand" : "swap_line_above" }, 29 | { "key": "selection_in_first_line", "operator": "equal", "operand": true }, 30 | { "key": "selection_in_last_line", "operator": "equal", "operand": false } 31 | ] 32 | }, 33 | { "keys": ["ctrl+super+down"], "command": "unswap_line_above", "context": 34 | [ 35 | { "key": "slm_settings.undo_swap_above", "operator": "equal", "operand": true }, 36 | { "key": "last_modifying_command", "operator": "equal", "operand" : "unswap_line_above" }, 37 | { "key": "selection_in_first_line", "operator": "equal", "operand": true }, 38 | { "key": "selection_in_last_line", "operator": "equal", "operand": false } 39 | ] 40 | }, 41 | { "keys": ["ctrl+super+up"], "command": "unswap_line_below", "context": 42 | [ 43 | { "key": "slm_settings.undo_swap_below", "operator": "equal", "operand": true }, 44 | { "key": "last_modifying_command", "operator": "equal", "operand" : "swap_line_below" }, 45 | { "key": "selection_in_last_line", "operator": "equal", "operand": true }, 46 | { "key": "selection_in_first_line", "operator": "equal", "operand": false } 47 | ] 48 | }, 49 | { "keys": ["ctrl+super+up"], "command": "unswap_line_below", "context": 50 | [ 51 | { "key": "slm_settings.undo_swap_below", "operator": "equal", "operand": true }, 52 | { "key": "last_modifying_command", "operator": "equal", "operand" : "unswap_line_below" }, 53 | { "key": "selection_in_last_line", "operator": "equal", "operand": true }, 54 | { "key": "selection_in_first_line", "operator": "equal", "operand": false }, 55 | ] 56 | }, 57 | { "keys": ["ctrl+alt+super+up"], "command": "separate_text_up" }, 58 | { "keys": ["ctrl+alt+super+down"], "command": "separate_text_down" }, 59 | { "keys": ["ctrl+alt+super+right"], "command": "repel_text" }, 60 | { "keys": ["ctrl+alt+super+left"], "command": "attract_text" }, 61 | 62 | ] 63 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | { "keys": ["ctrl+shift+up"], "command": "smart_swap_line_up" , "context": 4 | [ 5 | { "key": "slm_settings.smart_swap_up", "operator": "equal", "operand": true }, 6 | ] 7 | }, 8 | { "keys": ["ctrl+shift+down"], "command": "smart_swap_line_down" , "context": 9 | [ 10 | { "key": "slm_settings.smart_swap_down", "operator": "equal", "operand": true }, 11 | ] 12 | }, 13 | { "keys": ["ctrl+shift+up"], "command": "swap_line_above", "context": 14 | [ 15 | { "key": "slm_settings.swap_above", "operator": "equal", "operand": true }, 16 | { "key": "selection_in_first_line", "operator": "equal", "operand": true } 17 | ] 18 | }, 19 | { "keys": ["ctrl+shift+down"], "command": "swap_line_below", "context": 20 | [ 21 | { "key": "slm_settings.swap_below", "operator": "equal", "operand": true }, 22 | { "key": "selection_in_last_line", "operator": "equal", "operand": true } 23 | ] 24 | }, 25 | { "keys": ["ctrl+shift+down"], "command": "unswap_line_above", "context": 26 | [ 27 | { "key": "slm_settings.undo_swap_above", "operator": "equal", "operand": true }, 28 | { "key": "last_modifying_command", "operator": "equal", "operand" : "swap_line_above" }, 29 | { "key": "selection_in_first_line", "operator": "equal", "operand": true }, 30 | { "key": "selection_in_last_line", "operator": "equal", "operand": false } 31 | ] 32 | }, 33 | { "keys": ["ctrl+shift+down"], "command": "unswap_line_above", "context": 34 | [ 35 | { "key": "slm_settings.undo_swap_above", "operator": "equal", "operand": true }, 36 | { "key": "last_modifying_command", "operator": "equal", "operand" : "unswap_line_above" }, 37 | { "key": "selection_in_first_line", "operator": "equal", "operand": true }, 38 | { "key": "selection_in_last_line", "operator": "equal", "operand": false } 39 | ] 40 | }, 41 | { "keys": ["ctrl+shift+up"], "command": "unswap_line_below", "context": 42 | [ 43 | { "key": "slm_settings.undo_swap_below", "operator": "equal", "operand": true }, 44 | { "key": "last_modifying_command", "operator": "equal", "operand" : "swap_line_below" }, 45 | { "key": "selection_in_last_line", "operator": "equal", "operand": true }, 46 | { "key": "selection_in_first_line", "operator": "equal", "operand": false } 47 | ] 48 | }, 49 | { "keys": ["ctrl+shift+up"], "command": "unswap_line_below", "context": 50 | [ 51 | { "key": "slm_settings.undo_swap_below", "operator": "equal", "operand": true }, 52 | { "key": "last_modifying_command", "operator": "equal", "operand" : "unswap_line_below" }, 53 | { "key": "selection_in_last_line", "operator": "equal", "operand": true }, 54 | { "key": "selection_in_first_line", "operator": "equal", "operand": false }, 55 | ] 56 | }, 57 | { "keys": ["ctrl+alt+shift+up"], "command": "separate_text_up" }, 58 | { "keys": ["ctrl+alt+shift+down"], "command": "separate_text_down" }, 59 | { "keys": ["ctrl+alt+shift+right"], "command": "repel_text" }, 60 | { "keys": ["ctrl+alt+shift+left"], "command": "attract_text" }, 61 | 62 | ] 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Timo Rychert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": [ 7 | { 8 | "caption": "Package Settings", 9 | "mnemonic": "P", 10 | "id": "package-settings", 11 | "children": [ 12 | { 13 | "caption": "SmarterLineMoves", 14 | "children": [ 15 | { 16 | "caption": "Settings", 17 | "command": "edit_settings", 18 | "args": { 19 | "base_file": "${packages}/SmarterLineMoves/SmarterLineMoves.sublime-settings", 20 | "default": "{\n\t$0\n}\n" 21 | } 22 | 23 | }, 24 | { "caption": "-" }, 25 | { 26 | "caption": "Documentation", 27 | "command": "open_url", 28 | "args": { 29 | "url": "https://github.com/trych/SmarterLineMoves#readme" 30 | }, 31 | }, 32 | ] 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smarter Line Moves 2 | A Sublime Text package that overrides Sublime's default line moving to work in a more predictable way. 3 | 4 | 5 | ## Overview 6 | Sublime's default commands for moving lines – `Swap Line Up` and `Swap Line Down` – move files in a way that makes it difficult for the user to see where the text ends up once the selected text reaches the top or bottom of the window. This package fixes that by always keeping a few lines of space between the edges of the window and the moving text. This way, it is much easier to move the text into its dedicated position. 7 | 8 | ![Default swapping vs. smart swapping](https://user-images.githubusercontent.com/9803905/141032555-1bb01e54-7c68-43cd-86b1-ca0697b1f889.gif) 9 | 10 | Additionally, Sublime's default swapping would stop at the top or bottom of the text buffer and not let the user move the text any further. This package fixes that by inserting new empty lines and therefore allowing the selected text to move above or below the original text. In case the text has been moved too far, the package allows to move the text back and will automatically delete any previously added empty lines. 11 | 12 | ![Default swapping vs. swapping below](https://user-images.githubusercontent.com/9803905/141029958-b9c9919a-7fea-4013-91a2-8027243fe9d8.gif) 13 | 14 | The package offers the commands `Separate Text Up` and `Separate Text Down` to "separate" the selected text from the text above or below it. This is similar to Sublime Text's default `Insert Line After` and `Insert Line Before` commands, except that the selected text remains selected, so it appears like you move the selected text. 15 | 16 | To move text before *and* after the text selection simultaneously, the package adds the commands `Attract Text` and `Repel Text` that add or remove empty lines around the selected text respectively. 17 | 18 | ![Separate Up/Down & Attract/Repel Text](https://user-images.githubusercontent.com/9803905/143150992-5bf60700-b73f-4d6b-bd55-bff4a563b760.gif) 19 | 20 | ------------------------------------------------------------------------------- 21 | 22 | 23 | ## Installation ## 24 | 25 | ### Via Package Control ### 26 | 27 | The best way to install the package is via Sublime's Package Control. This way the package will automatically keep up to date if there are new versions. 28 | 29 | To install via Package Control, open the Command Palette and select the command `Package Control: Install Package` and search for `SmarterLineMoves`. 30 | 31 | ### Manually ### 32 | 33 | You can install the package manually by [downloading the repo](https://api.github.com/repos/trych/SmarterLineMoves/zipball) and placing it in your Sublime Text `User` Package, which you can find by using `Preferences > Browse Packages...`. Just unzip the file and place it in the `User` folder. This installation method is not recommended, as the package will not automatically be updated. 34 | 35 | 36 | ------------------------------------------------------------------------------- 37 | 38 | 39 | ## Usage 40 | 41 | ### Smart Swapping 42 | 43 | As the package overrides Sublime's default swapping commands, you can use the smarter swapping by simply using the regular shortcuts: Shift+Ctrl+Up/Down on Windows/Linux or ⌘+Ctrl+Up/Down on macOS. The package will take care of the rest, keep the space between the selected text and the window edges or let the selected text move above or below the beginning and end of the text. 44 | 45 | ### Separate Text Up/Down 46 | 47 | Use the Shift+Ctrl+Alt+Up/Down keys on Windows/Linux or ⌘+Ctrl+Alt+Up/Down on macOS to separate the selected text up or down respectively. 48 | 49 | ### Attract/Repel Text 50 | 51 | Use the Shift+Ctrl+Alt+Right keys on Windows/Linux or ⌘+Ctrl+Alt+Right on macOS to to "repel" text from the current text selection and the Shift+Ctrl+Alt+Left keys on Windows/Linux or ⌘+Ctrl+Alt+Left on macOS to "attract" text towards the current text selection. 52 | 53 | ------------------------------------------------------------------------------- 54 | 55 | 56 | ## Configuration 57 | 58 | ### Settings 59 | 60 | The package's features can be changed and/or disabled by changing its settings. 61 | 62 | You can open the settings file to see the default settings or change them to your custom settings under the `Preferences > Package Settings > SmarterLineMoves` menu entry. The settings file has the following entries: 63 | 64 | #### `smart_swap_up`: true/false (Default: true) 65 | 66 | Turns the smart swapping in the up direction on or off. If it is turned off, Sublime's regular `Swap Line Up` command will be used again. 67 | 68 | #### `smart_swap_down`: true/false (Default: true) 69 | 70 | Turns the smart swapping in the down direction on or off. If it is turned off, Sublime's regular `Swap Line Down` command will be used again. 71 | 72 | #### `swap_above`: true/false (Default: true) 73 | 74 | Allows the text to move "above" the text buffer once the moving text reaches the top of the file by adding empty lines that the text can be swapped with, so it just keeps moving up when repeating the command. 75 | 76 | #### `undo_swap_above`: true/false (Default: true) 77 | 78 | If the selected text has been moved up "above" the text buffer too far, it can be moved back by using the `Swap Line Down` key binding. If this setting is set to true, the empty lines that have been previously added, will be automatically removed again. 79 | 80 | #### `swap_below`: true/false (Default: true) 81 | 82 | Allows the text to move "below" the text buffer once the moving text reaches the bottom of the file by adding empty lines that the text can be swapped with, so it just keeps moving down when repeating the command. 83 | 84 | #### `undo_swap_below`: true/false (Default: true) 85 | 86 | If the selected text has been moved down "below" the text buffer too far, it can be moved back by using the `Swap Line Up` key binding. If this setting is set to true, the empty lines that have been previously added, will be automatically removed again. 87 | 88 | #### `move_up_clearance`: Number (Default: 5) 89 | 90 | How many lines to keep visible between the moving text and the window top when using the package's text moving commands. 91 | 92 | #### `move_down_clearance`: Number (Default: 5) 93 | 94 | How many lines to keep visible between the moving text and the window bottom when using the package's text moving commands. 95 | 96 | #### `auto_reindent`: true/false (Default: false) 97 | 98 | Will automatically reindent the selected text after smart swapping. 99 | 100 | #### `squash_whitespace_only_lines`: true/false (Default: true) 101 | 102 | If using the `Attract Text` command and this is set to true, lines that have only white space in them will be erased as well, as if they were empty lines. When this is set to false, those lines will be kept, just like regular lines with text content. 103 | 104 | ### Keyboard Shortcuts 105 | 106 | You can change the package's default keyboard shortcuts for the `Separate Text Up/Down` and the `Attract/Repel Text` commands by changing their key bindings. 107 | 108 | The default key bindings for Windows/Linux are: 109 | 110 | ```json 111 | { "keys": ["ctrl+alt+shift+up"], "command": "separate_text_up" }, 112 | { "keys": ["ctrl+alt+shift+down"], "command": "separate_text_down" }, 113 | { "keys": ["ctrl+alt+shift+right"], "command": "repel_text" }, 114 | { "keys": ["ctrl+alt+shift+left"], "command": "attract_text" }, 115 | ``` 116 | 117 | The default key bindings for macOS are: 118 | 119 | ```json 120 | { "keys": ["ctrl+alt+super+up"], "command": "separate_text_up" }, 121 | { "keys": ["ctrl+alt+super+down"], "command": "separate_text_down" }, 122 | { "keys": ["ctrl+alt+super+right"], "command": "repel_text" }, 123 | { "keys": ["ctrl+alt+super+left"], "command": "attract_text" }, 124 | ``` 125 | 126 | If you want to change the keyboard shortcut for the `Attract Text` command to Shift+Ctrl+A for example, you can add the following line to your User keybinding map (which you can open via `Preferences > Key Bindings`): 127 | 128 | ```json 129 | { "keys": ["ctrl+shift+a"], "command": "attract_text" }, 130 | ``` 131 | 132 | ------------------------------------------------------------------------------- 133 | 134 | 135 | ## Issues and Feedback 136 | 137 | If you run into any issues using SmarterLineMoves or you have an idea for additional features, feel free to [open an issue in the package's issue tracker](https://github.com/trych/SmarterLineMoves/issues). 138 | 139 | 140 | ------------------------------------------------------------------------------- 141 | 142 | 143 | ## License 144 | 145 | SmarterLineMoves is licensed under the [MIT License](LICENSE). 146 | -------------------------------------------------------------------------------- /SmarterLineMoves.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // Smart swapping means that there will always be a few lines visible 3 | // between the uppermost moving line and the text window's top, or 4 | // the bottommost moving line and the text window's bottom, depending 5 | // on the direction of movement. This allows to better see where 6 | // the lines will end up after swapping. 7 | 8 | // The smart swapping settings change the behavior of Sublime Text's 9 | // default line swapping commands (Ctrl+Shift+Up/Down on Windows/Linux 10 | // or Ctrl+Cmd+Up/Down on Mac). 11 | 12 | // Turns smart swapping on when swapping lines up. 13 | "smart_swap_up": true, 14 | 15 | // Turns smart swapping on when swapping lines up. 16 | "smart_swap_down": true, 17 | 18 | // Allowing to swap text "above and below" means that the text 19 | // swapping does not stop once it hits the top or bottom of the 20 | // window, but that the selected text keeps moving with each further 21 | // execution of the swapping command by adding empty lines on the 22 | // top/bottom of the selection. 23 | // 24 | // The corresponding undo commands allow to swap the text back in the 25 | // other direction while removing the previously added empty lines. 26 | // This is helpful to move text back after it has been moved too 27 | // far. 28 | "swap_above": true, 29 | "undo_swap_above": true, 30 | 31 | "swap_below": true, 32 | "undo_swap_below": true, 33 | 34 | // How many lines to keep visible when moving lines up. 35 | "move_up_clearance": 5, 36 | 37 | // How many lines to keep visible when moving lines down. 38 | "move_down_clearance": 5, 39 | 40 | // Automatically re-indents text after moving 41 | "auto_reindent": false, 42 | 43 | // Allows lines that contain only whitespace to be deleted while using 44 | // the "Attract Text" command. 45 | "squash_whitespace_only_lines": true, 46 | } 47 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "1.1.0": "messages/1.1.0.txt", 4 | "1.1.1": "messages/1.1.1.txt", 5 | "1.1.2": "messages/1.1.2.txt" 6 | } -------------------------------------------------------------------------------- /messages/1.1.0.txt: -------------------------------------------------------------------------------- 1 | Version 1.1.0 (2021-12-14) 2 | -------------------------- 3 | 4 | * Adds auto re-indent option to automatically re-indent text after 5 | smart swapping (turned off by default, turn on in 6 | Preferences -> Package Settings -> SmarterLineMoves -> Settings) 7 | -------------------------------------------------------------------------------- /messages/1.1.1.txt: -------------------------------------------------------------------------------- 1 | Version 1.1.1 (2021-01-02) 2 | -------------------------- 3 | 4 | * Fix: Distance between moving text and the window top is now also correctly inserted on un-doing a text move below the text buffer. 5 | -------------------------------------------------------------------------------- /messages/1.1.2.txt: -------------------------------------------------------------------------------- 1 | Version 1.1.2 (2022-10-18) 2 | -------------------------- 3 | 4 | * Fix: Replace default platform agnostic keymap by platform specific keymaps for Windows and Linux to avoid multiple keyboard shortcuts on macOS. 5 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | 2 | SmarterLineMoves 3 | ================ 4 | 5 | A Sublime Text package that allows to move your text lines in a smarter way. 6 | 7 | Comes with the following features: 8 | 9 | * SmartSwap: Always keep a few lines of space between your moving text and the 10 | text window's edges. 11 | * Swap Above/Below: Allows to keep the text moving, even after it hits the top 12 | or bottom of the text window. 13 | * Separate Text Up/Down: Adds a command to separate selected text from the 14 | neighboring lines above or below it. 15 | * Attract/Repel Text: Moves neighboring lines towards to or away from the 16 | selected text. 17 | 18 | + Shortcuts: 19 | 20 | For Windows/Linux: 21 | * Smart Swap & Swap Above/Below: Shift+Ctrl+Up/Down 22 | * Separate Text Up: Shift+Ctrl+Alt+Up 23 | * Separate Text Down: Shift+Ctrl+Alt+Down 24 | * Attract Text: Shift+Ctrl+Alt+Left 25 | * Repel Text: Shift+Ctrl+Alt+Right 26 | 27 | For macOS: 28 | * Smart Swap & Swap Above/Below: Ctrl+Cmd+Up/Down 29 | * Separate Text Up: Ctrl+Alt+Cmd+Up 30 | * Separate Text Down: Ctrl+Alt+Cmd+Down 31 | * Attract Text: Ctrl+Alt+Cmd+Left 32 | * Repel Text: Ctrl+Alt+Cmd+Right 33 | 34 | For detailed documentation see: https://github.com/trych/SmarterLineMoves 35 | -------------------------------------------------------------------------------- /smarter_line_moves.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | 5 | slm_settings = {} 6 | 7 | 8 | def plugin_loaded(): 9 | global slm_settings 10 | slm_settings = sublime.load_settings('SmarterLineMoves.sublime-settings') 11 | 12 | 13 | class SmartSwapLineUpCommand(sublime_plugin.TextCommand): 14 | """ 15 | Swaps lines up while keeping a set amount of lines visible between 16 | the uppermost moving selected line and the text window's top. 17 | """ 18 | def run(self, edit): 19 | self.view.run_command('swap_line_up') 20 | clear_top(self.view) 21 | if slm_settings.get('auto_reindent'): 22 | self.view.run_command('reindent') 23 | 24 | 25 | class SmartSwapLineDownCommand(sublime_plugin.TextCommand): 26 | """ 27 | Swaps lines down while keeping a set amount of lines visible between 28 | the bottommost moving selected line and the text window's bottom. 29 | """ 30 | def run(self, edit): 31 | self.view.run_command('swap_line_down') 32 | clear_bottom(self.view) 33 | if slm_settings.get('auto_reindent'): 34 | self.view.run_command('reindent') 35 | 36 | 37 | class SwapLineAboveCommand(sublime_plugin.TextCommand): 38 | """ 39 | Extends the Swap Line Up Command so that it keeps moving the 40 | selection even after hitting the top of the text buffer by adding 41 | an extra empty line on top that gets swapped. 42 | """ 43 | def run(self, edit): 44 | self.view.insert(edit, 0, '\n') 45 | self.view.run_command('swap_line_up') 46 | 47 | 48 | class SwapLineBelowCommand(sublime_plugin.TextCommand): 49 | """ 50 | Extends the Swap Line Down Command so that it keeps moving the 51 | selection even after hitting the bottom of the text buffer by adding 52 | an extra empty line on the bottom that gets swapped. 53 | """ 54 | def run(self, edit): 55 | self.view.insert(edit, len(self.view), '\n') 56 | self.view.run_command('swap_line_down') 57 | clear_bottom(self.view) 58 | 59 | 60 | class UnswapLineAbove(sublime_plugin.TextCommand): 61 | """ 62 | Undoes the swap line above command by moving the line back down 63 | and deleting the previously added empty lines. 64 | """ 65 | def run(self, edit): 66 | self.view.run_command('swap_line_down') 67 | 68 | if self.view.line(0).empty(): 69 | self.view.erase(edit, sublime.Region(0, 1)) 70 | 71 | 72 | class UnswapLineBelowCommand(sublime_plugin.TextCommand): 73 | """ 74 | Undoes the swap line below command by moving the line back up 75 | and deleting the previously added empty lines. 76 | """ 77 | def run(self, edit): 78 | view = self.view 79 | 80 | view.run_command('swap_line_up') 81 | clear_top(view) 82 | 83 | if view.line(view.size()).empty(): 84 | view.erase(edit, sublime.Region(len(view) - 1, len(view))) 85 | 86 | 87 | 88 | class SeparateTextUp(sublime_plugin.TextCommand): 89 | """ 90 | Separates selected text from the text below it by inserting 91 | empty lines below the text, thus moving the selected text up. 92 | """ 93 | def run(self, edit): 94 | view = self.view 95 | 96 | sel_end = view.sel()[-1].end() 97 | sel_last_line = view.rowcol(sel_end)[0] 98 | insertPoint = view.text_point(sel_last_line + 1, 0) 99 | 100 | view.insert(edit, insertPoint, '\n') 101 | 102 | clear_top(view, True) 103 | 104 | 105 | class SeparateTextDown(sublime_plugin.TextCommand): 106 | """ 107 | Separates selected text from the text above it by inserting 108 | empty lines above the text, thus moving the selected text down. 109 | """ 110 | def run(self, edit): 111 | view = self.view 112 | lh = view.line_height() 113 | 114 | sel_begin = view.sel()[0].begin() 115 | sel_first_line = view.rowcol(sel_begin)[0] 116 | insertPoint = view.text_point(sel_first_line, 0) 117 | 118 | view.insert(edit, insertPoint, '\n') 119 | 120 | clear_bottom(view) 121 | 122 | 123 | class RepelText(sublime_plugin.TextCommand): 124 | """ 125 | Moves neighboring text away from the text selection by inserting 126 | empty lines around the text selection. 127 | """ 128 | def run(self, edit): 129 | view = self.view 130 | 131 | sel_begin = view.sel()[0].begin() 132 | begin_insert_point = view.text_point(view.rowcol(sel_begin)[0], 0) 133 | 134 | sel_end = view.sel()[-1].end() 135 | end_insert_point = view.text_point(view.rowcol(sel_end)[0] + 1, 0) 136 | 137 | view.insert(edit, begin_insert_point, '\n') 138 | view.insert(edit, end_insert_point, '\n') 139 | 140 | shift_view(view, 1) 141 | 142 | 143 | class AttractText(sublime_plugin.TextCommand): 144 | """ 145 | Pulls neighboring text towards the text selection by removing 146 | empty lines or – depending on the setting – whitespace only 147 | lines. 148 | """ 149 | def run(self, edit): 150 | view = self.view 151 | 152 | prev_line = view.line(view.text_point(view.rowcol(view.sel()[0].begin())[0] - 1, 0)) 153 | next_line = view.line(view.text_point(view.rowcol(view.sel()[-1].end())[0] + 1, 0)) 154 | 155 | if next_line.empty() or (slm_settings.get('squash_whitespace_only_lines') and view.substr(next_line).isspace()): 156 | view.erase(edit, sublime.Region(next_line.begin(), next_line.end() + 1)) 157 | 158 | if prev_line.empty() or (slm_settings.get('squash_whitespace_only_lines') and view.substr(prev_line).isspace()): 159 | first_line = view.rowcol(view.visible_region().begin())[0] 160 | 161 | view.erase(edit, sublime.Region(prev_line.begin(), prev_line.end() + 1)) 162 | 163 | shift_view(view, -1) 164 | 165 | 166 | class SelectedLinesContextEventListener(sublime_plugin.EventListener): 167 | """ 168 | Creates a custom context to allow key bindings to check if either the 169 | first or the last line is part of the current selection. 170 | """ 171 | def on_query_context(self, view, key, operator, operand, match_all): 172 | 173 | if key == 'selection_in_first_line': 174 | key_condition = view.line(view.sel()[0].begin()) == view.line(0) 175 | elif key == 'selection_in_last_line': 176 | key_condition = view.line(view.sel()[-1].end()) == view.line(len(view)) 177 | else: 178 | return None 179 | 180 | if operator == sublime.OP_EQUAL: 181 | return key_condition == operand 182 | elif operator == sublime.OP_NOT_EQUAL: 183 | return key_condition != operand 184 | 185 | return False 186 | 187 | 188 | class SmarterLineMovesSettingsEventListener(sublime_plugin.EventListener): 189 | """ 190 | Called to handle the custom slm_settings key in the package's keymap. 191 | """ 192 | def on_query_context(self, view, key, operator, operand, match_all): 193 | if not key.startswith('slm_settings.'): 194 | return None 195 | 196 | setting = key[len('slm_settings.'):] 197 | lhs = slm_settings.get(setting) 198 | 199 | if operator == sublime.OP_EQUAL: 200 | return lhs == operand 201 | elif operator == sublime.OP_NOT_EQUAL: 202 | return lhs != operand 203 | 204 | return False 205 | 206 | 207 | class SwapLineCommandEventListener(sublime_plugin.EventListener): 208 | """ 209 | An event listener that overwrites the default line swap commands 210 | when they are called via the menu entries Edit > Line > Swap Line Up/Down. 211 | """ 212 | def on_text_command(self, view, command_name, args): 213 | 214 | if command_name == 'swap_line_up' and slm_settings.get('smart_swap_up'): 215 | return ('smart_swap_line_up', {}) 216 | 217 | if command_name == 'swap_line_down' and slm_settings.get('smart_swap_down'): 218 | return ('smart_swap_line_down', {}) 219 | 220 | 221 | def shift_view(view, amt): 222 | current_pos = view.viewport_position() 223 | view.set_viewport_position((current_pos[0], current_pos[1] + (view.line_height() * amt)), False) 224 | 225 | 226 | def clear_top(view, inverse = False): 227 | first_line_pos = view.text_to_layout(view.sel()[0].begin())[1] - view.viewport_position()[1] 228 | min_pos = view.line_height() * slm_settings.get('move_up_clearance') 229 | 230 | if inverse and (first_line_pos - view.line_height()) > min_pos: 231 | shift_view(view, 1) 232 | 233 | if first_line_pos < min_pos: 234 | shift_amt = (min_pos - first_line_pos) // view.line_height() + 1 235 | shift_view(view, -shift_amt) 236 | 237 | 238 | def clear_bottom(view): 239 | last_line_pos = view.text_to_layout(view.sel()[-1].end())[1] - view.viewport_position()[1] + view.line_height() 240 | max_pos = view.viewport_extent()[1] - view.line_height() * slm_settings.get('move_down_clearance') 241 | 242 | if last_line_pos > max_pos: 243 | shift_amt = (last_line_pos - max_pos) // view.line_height() + 1 244 | shift_view(view, shift_amt) 245 | --------------------------------------------------------------------------------