├── .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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------