├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── envy.cson ├── lib ├── base-classes.js ├── block-selection.js ├── bracket-selection.js ├── clipboard.js ├── extent.js ├── main.js ├── match-selection.js ├── position.js ├── selection-inverter.js └── utilities.js ├── package.json ├── spec ├── envy-spec.js └── tests │ ├── align-selections.test │ ├── block-selection-1.test │ ├── block-selection-2.test │ ├── block-selection-3.test │ ├── bracket-selection-1.test │ ├── bracket-selection-2.test │ ├── bracket-selection-3.test │ ├── invert-selections.test │ ├── match-selection-1.test │ ├── match-selection-2.test │ ├── remove-selections.test │ ├── rotate-selections.test │ ├── secondary-clipboard.test │ ├── split-selections.test │ └── swap-selections.test └── styles └── envy.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Source: https://raw.githubusercontent.com/atom/ci/master/.travis.yml 2 | 3 | ### Project specific config ### 4 | language: generic 5 | 6 | env: 7 | global: 8 | - APM_TEST_PACKAGES="" 9 | - ATOM_LINT_WITH_BUNDLED_NODE="true" 10 | 11 | matrix: 12 | - ATOM_CHANNEL=stable 13 | - ATOM_CHANNEL=beta 14 | 15 | os: 16 | - linux 17 | - osx 18 | 19 | dist: trusty 20 | 21 | ### Generic setup follows ### 22 | script: 23 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 24 | - chmod u+x build-package.sh 25 | - ./build-package.sh 26 | 27 | notifications: 28 | email: 29 | on_success: never 30 | on_failure: change 31 | 32 | branches: 33 | only: 34 | - master 35 | 36 | git: 37 | depth: 10 38 | 39 | sudo: false 40 | 41 | addons: 42 | apt: 43 | packages: 44 | - build-essential 45 | - git 46 | - libgnome-keyring-dev 47 | - libsecret-1-dev 48 | - fakeroot 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.2.2 (2017-05-06) 2 | * Fix recursive definition if theme declares variable "@background" ([#8](https://github.com/p-e-w/envy/issues/8)) 3 | 4 | 5 | ## v0.2.1 (2017-04-29) 6 | * Guard against bracket-matcher package being unavailable 7 | 8 | 9 | ## v0.2.0 (2017-04-22) 10 | * Fix several bugs 11 | * Add tests 12 | * Add documentation 13 | 14 | 15 | ## v0.1.0 (2017-03-12) 16 | * Initial release 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Philipp Emanuel Weidmann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Envy

2 |

Text editing supercharger

3 | 4 | [![Build Status](https://travis-ci.org/p-e-w/envy.svg?branch=master)](https://travis-ci.org/p-e-w/envy) 5 | 6 | Envy is an [Atom](https://github.com/atom/atom) :atom: package that provides a switchable mode in which letter keypresses are mapped to text editing operations. 7 | 8 | ![Screencast](https://cloud.githubusercontent.com/assets/2702526/25304025/4a1e3a0e-274e-11e7-9504-3cf802405cbe.gif) 9 | 10 | This approach is shared with the [Vim](http://www.vim.org) family of editors, but only conceptually. Envy doesn't have any key bindings in common with Vim and augments, rather than modifies, the standard Atom editing experience. It simply wouldn't make sense to create yet another Vim clone or derivative – there are already so many. Instead, Envy is built from scratch to be an ergonomic modal editing system without any legacy baggage. 11 | 12 | Where applicable, Envy's design choices seek to avoid problems associated with Vim and its descendants. _Envy = N.V. = **N**ot **V**im._ Unlike Vim/[vim-mode](https://github.com/atom/vim-mode), Envy 13 | 14 | * takes mere minutes to learn and master 15 | * embraces multiple selections 16 | * has a logical, easy to remember [keyboard layout](#keymap) designed with ergonomics in mind 17 | * has no Ctrl/Cmd key bindings and therefore doesn't interfere with other editor shortcuts 18 | * works great with [international keyboards](#using-envy-with-non-us-keyboards) :earth_africa: 19 | 20 | Envy offers a much smaller set of operations than Vim, but the easy accessibility of those operations and their tight integration with Atom's editing model means they can be used to their fullest, making Atom+Envy a competitive alternative to established modal text editors. 21 | 22 | 23 | ## Installation 24 | 25 | Either run 26 | 27 | ``` 28 | apm install envy 29 | ``` 30 | 31 | from the command line or search for `envy` in Atom's *Install Packages* settings screen and click the *Install* button on the package card. 32 | 33 | 34 | ## Keymap 35 | 36 | Vim's default keymap is largely based on mnemonic abbreviations, that is, the letter pressed is found in the English name for the action performed. For example, in Vim, B and E move the cursor to the _**b**eginning_ and _**e**nd_ of the word, respectively. 37 | 38 | By contrast, **Envy's keymap is based on the _location of keys on the keyboard._** The keyboard is divided into connected groups of keys that perform related actions in a consistent and predictable manner. Thus, on the default QWERTY layout, E/R move the selection to the previous/next bracket, D/F move it to the previous/next *block* (word, line or paragraph), and C/V move it to the previous/next match. For layouts other than QWERTY, [patches are provided](#using-envy-with-non-us-keyboards) that produce the same key arrangement. 39 | 40 | **Alt+J enters Envy mode and P leaves it.** In Envy mode, this keymap applies: 41 | 42 |
43 | 44 | ![Keymap](https://cloud.githubusercontent.com/assets/2702526/25304030/6ce2d1b2-274e-11e7-9e4e-a3910a8b6765.png) 45 | 46 | *(Based on https://commons.wikimedia.org/wiki/File:KB_United_States.svg by Denelson83.)* 47 | 48 |
49 | 50 | A detailed listing of all commands and their mapping follows. Note that the *Key* column shows the label of the letter key in the QWERTY keyboard layout, which may be different in another layout (but the physical position of the key is the same across all layouts). 51 | 52 | ### Cursor movement 53 | 54 | #### Arrow keys 55 | 56 | | Key | Command | Description | 57 | | --- | --- | --- | 58 | | J | **Move left** | :arrow_left: Moves each cursor one position to the left.
**With Shift:** Selects instead of moving.
**With Alt:** Moves one subword boundary instead of one position. | 59 | | L | **Move right** | :arrow_right: Moves each cursor one position to the right.
**With Shift:** Selects instead of moving.
**With Alt:** Moves one subword boundary instead of one position. | 60 | | I | **Move up** | :arrow_up: Moves each cursor one line up.
**With Shift:** Selects instead of moving.
**With Alt:** Adds a selection above each selection instead of moving cursors. Cannot be combined with Shift. | 61 | | K | **Move down** | :arrow_down: Moves each cursor one line down.
**With Shift:** Selects instead of moving.
**With Alt:** Adds a selection below each selection instead of moving cursors. Cannot be combined with Shift. | 62 | 63 | #### Other 64 | 65 | | Key | Command | Description | 66 | | --- | --- | --- | 67 | | Y | **Move to beginning of previous paragraph** | Moves each cursor to the start of the paragraph preceding the cursor.
**With Shift:** Selects instead of moving. | 68 | | H | **Move to beginning of next paragraph** | Moves each cursor to the start of the paragraph succeeding the cursor.
**With Shift:** Selects instead of moving. | 69 | | Alt+Y | **Page up** | Moves each cursor one editor page up.
**With Shift:** Selects instead of moving. | 70 | | Alt+H | **Page down** | Moves each cursor one editor page down.
**With Shift:** Selects instead of moving. | 71 | | U | **Move to beginning of line** | Moves each cursor to the start of the line it is on.
**With Shift:** Selects instead of moving. | 72 | | O | **Move to end of line** | Moves each cursor to the end of the line it is on.
**With Shift:** Selects instead of moving. | 73 | 74 | ### Selection manipulation 75 | 76 | #### Brackets 77 | 78 | Supported bracket pairs include all pairs defined by the [bracket-matcher](https://github.com/atom/bracket-matcher) Atom core package. 79 | 80 | | Key | Command | Description | 81 | | --- | --- | --- | 82 | | W | **Select surrounding brackets** | Expands each selection to cover the inside of the nearest surrounding bracket pair. If a selection already completely covers the inside of a bracket pair, expands the selection to the outside of that bracket pair. | 83 | | Shift+W | **Select all brackets** | Repeatedly applies the **Move bracket selection backward/forward** commands (see below) to each selection and creates a selection containing all of the results. | 84 | | E | **Move bracket selection backward** | Moves each selection to the outside of the first bracket pair that completely precedes the selection.
**With Shift:** Preserves the existing selection. | 85 | | R | **Move bracket selection forward** | Moves each selection to the outside of the first bracket pair that completely succeeds the selection.
**With Shift:** Preserves the existing selection. | 86 | 87 | #### Blocks 88 | 89 | In Envy, a *block* is either 90 | 91 | * a homogeneous sequence of word characters, non-word characters (excluding whitespace), or whitespace characters (e.g. `abc`, `{.-` and ` `) contained within a single line of text, 92 | * a single line of text, or 93 | * a single paragraph of text. 94 | 95 | | Key | Command | Description | 96 | | --- | --- | --- | 97 | | S | **Select surrounding block** | Expands each selection to cover the nearest block that completely contains the selection. | 98 | | Shift+S | **Select all blocks** | Repeatedly applies the **Move block selection backward/forward** commands (see below) to each selection and creates a selection containing all of the results. | 99 | | D | **Move block selection backward** | Moves each selection to the first block of the same type that completely precedes the selection.
**With Shift:** Preserves the existing selection.
**With Alt:** Considers all first-tier blocks (runs of word/non-word/whitespace characters) to be of the same type. | 100 | | F | **Move block selection forward** | Moves each selection to the first block of the same type that completely succeeds the selection.
**With Shift:** Preserves the existing selection.
**With Alt:** Considers all first-tier blocks (runs of word/non-word/whitespace characters) to be of the same type. | 101 | 102 | #### Matches 103 | 104 | | Key | Command | Description | 105 | | --- | --- | --- | 106 | | X | **Select all matches** | For each selection, selects all occurrences of the selected text in the lines covered by the selection.
**With Shift:** Selects all matches in the entire buffer instead. | 107 | | C | **Select previous match** | Moves each selection to the previous occurrence of the selected text.
**With Shift:** Preserves the existing selection. | 108 | | V | **Select next match** | Moves each selection to the next occurrence of the selected text.
**With Shift:** Preserves the existing selection. | 109 | 110 | #### Other 111 | 112 | | Key | Command | Description | 113 | | --- | --- | --- | 114 | | Q | **Remove every second selection** | Drops all selections (*not* the selection contents) with even-numbered indices, where the first selection has index 1.
**With Shift:** Drops odd-numbered selections instead. | 115 | | A | **Consolidate selections** | Drops all selections except the first one. | 116 | | Shift+A | **Split selections into cursors** | Turns every non-empty selection into two cursors, one at the start of the selection and one at the end. | 117 | | Z | **Invert selections** | Selects all unselected text and deselects all selected text in the lines covered by selections.
**With Shift:** Inverts selections in the entire buffer instead. | 118 | 119 | ### Text manipulation 120 | 121 | | Key | Command | Description | 122 | | --- | --- | --- | 123 | | N | **Swap selections** | Assuming the selections are indexed starting from 1, swaps the contents of selections 1 and 2, 3 and 4, etc.
**With Shift:** Swaps selections 1 and 3, 2 and 4, etc. instead. | 124 | | Alt+N | **Rotate selections** | Cyclically replaces the contents of each selection with those of the preceding selection.
**With Shift:** Uses the contents of the succeeding selection instead. | 125 | | M | **Align selections** | Left-aligns all selections by inserting spaces to the left of them. If there are multiple selections on a single line, those selections are aligned with the corresponding selections on other lines.
**With Shift:** Right-aligns selections instead. | 126 | 127 | ### Clipboard 128 | 129 | | Key | Command | Description | 130 | | --- | --- | --- | 131 | | T | **Copy** | Copies the selected text to the system clipboard.
**With Shift:** Uses Envy's internal ("secondary") clipboard instead. | 132 | | G | **Paste** | Pastes the text from the system clipboard.
**With Shift:** Uses Envy's internal ("secondary") clipboard instead. | 133 | | B | **Cut** | Cuts the selected text to the system clipboard.
**With Shift:** Uses Envy's internal ("secondary") clipboard instead. | 134 | 135 | 136 | ## Using Envy with non-US keyboards 137 | 138 | Envy's keymap consists exclusively of letters. As the shapes and positions of the letter keys are identical for all standard keyboards (as opposed to, say, the Enter and left Shift keys), it is possible to achieve exactly the same Envy mode layout regardless of the keyboard layout used. 139 | 140 | The only thing needed is to swap the bindings for any letter keys that in the local keyboard layout are rearranged compared to Envy's default layout (QWERTY). Most regional layouts have only minor rearrangements, and the necessary changes can be easily achieved by adding a "patch" to Atom's `keymap.cson`. 141 | 142 | ### :de: QWERTZ keymap patch 143 | 144 | [QWERTZ](https://en.wikipedia.org/wiki/QWERTZ) keyboard layouts are used mainly in the German-speaking world and in parts of Eastern Europe. As far as letter keys go, QWERTZ differs from QWERTY only in that Y and Z are exchanged, so the following will make Envy use its [default positional arrangement](#keymap) on a QWERTZ keyboard: 145 | 146 | ```coffeescript 147 | 'atom-text-editor.envy-mode': 148 | 'y': 'envy:invert-selections-in-lines' 149 | 'Y': 'envy:invert-selections' 150 | 'z': 'editor:move-to-beginning-of-previous-paragraph' 151 | 'Z': 'editor:select-to-beginning-of-previous-paragraph' 152 | 'alt-z': 'core:page-up' 153 | 'alt-Z': 'core:select-page-up' 154 | ``` 155 | 156 | ### :fr: AZERTY keymap patch 157 | 158 | [AZERTY](https://en.wikipedia.org/wiki/AZERTY) is the predominant keyboard layout in France, and is also used by French speakers in Belgium and some other countries. Compared to QWERTY, it swaps A and Q, W and Z, and has ,/? in place of M, giving rise to this patch: 159 | 160 | ```coffeescript 161 | 'atom-text-editor.envy-mode': 162 | 'a': 'envy:remove-every-second-selection-even' 163 | 'A': 'envy:remove-every-second-selection-odd' 164 | 'q': 'editor:consolidate-selections' 165 | 'Q': 'envy:split-selections-into-cursors' 166 | 'w': 'envy:invert-selections-in-lines' 167 | 'W': 'envy:invert-selections' 168 | 'z': 'envy:select-surrounding-brackets' 169 | 'Z': 'envy:select-all-brackets' 170 | ',': 'envy:left-align-selections' 171 | '?': 'envy:right-align-selections' 172 | ``` 173 | 174 | 175 | ## Contributing 176 | 177 | Contributors are always welcome. However, **please file an issue describing what you intend to add before opening a pull request,** *especially* for new features! I have a clear vision of what I want (and do not want) Envy to be, so discussing potential additions might help you avoid duplication and wasted work. 178 | 179 | By contributing, you agree to release your changes under the same license as the rest of the project (see below). 180 | 181 | 182 | ## License 183 | 184 | Copyright © 2017 Philipp Emanuel Weidmann () 185 | 186 | Released under the terms of the [MIT License](https://opensource.org/licenses/MIT) 187 | -------------------------------------------------------------------------------- /keymaps/envy.cson: -------------------------------------------------------------------------------- 1 | 'atom-text-editor:not(.envy-mode)': 2 | 'alt-j': 'envy:toggle' 3 | 4 | # Default Envy mode keymap (QWERTY layout) 5 | 'atom-text-editor.envy-mode': 6 | 'p': 'envy:toggle' 7 | 8 | # Selections 9 | 'q': 'envy:remove-every-second-selection-even' 10 | 'Q': 'envy:remove-every-second-selection-odd' 11 | 12 | 'a': 'editor:consolidate-selections' 13 | 'A': 'envy:split-selections-into-cursors' 14 | 15 | 'z': 'envy:invert-selections-in-lines' 16 | 'Z': 'envy:invert-selections' 17 | 18 | 'w': 'envy:select-surrounding-brackets' 19 | 'W': 'envy:select-all-brackets' 20 | 'e': 'envy:move-bracket-selection-backward' 21 | 'E': 'envy:add-bracket-selection-backward' 22 | 'r': 'envy:move-bracket-selection-forward' 23 | 'R': 'envy:add-bracket-selection-forward' 24 | 25 | 's': 'envy:select-surrounding-block' 26 | 'S': 'envy:select-all-blocks' 27 | 'd': 'envy:move-block-selection-backward' 28 | 'D': 'envy:add-block-selection-backward' 29 | 'alt-d': 'envy:move-block-selection-backward-alternative' 30 | 'alt-D': 'envy:add-block-selection-backward-alternative' 31 | 'f': 'envy:move-block-selection-forward' 32 | 'F': 'envy:add-block-selection-forward' 33 | 'alt-f': 'envy:move-block-selection-forward-alternative' 34 | 'alt-F': 'envy:add-block-selection-forward-alternative' 35 | 36 | 'x': 'envy:select-all-matches-in-lines' 37 | 'X': 'envy:select-all-matches' 38 | 'c': 'envy:select-previous-match' 39 | 'C': 'envy:add-select-previous-match' 40 | 'v': 'envy:select-next-match' 41 | 'V': 'envy:add-select-next-match' 42 | 43 | # Clipboard 44 | 't': 'core:copy' 45 | 'T': 'envy:copy-to-secondary-clipboard' 46 | 'g': 'core:paste' 47 | 'G': 'envy:paste-from-secondary-clipboard' 48 | 'b': 'core:cut' 49 | 'B': 'envy:cut-to-secondary-clipboard' 50 | 51 | # Text manipulation 52 | 'n': 'envy:swap-selections' 53 | 'N': 'envy:swap-selections-alternative' 54 | 'alt-n': 'envy:rotate-selections-forward' 55 | 'alt-N': 'envy:rotate-selections-backward' 56 | 'm': 'envy:left-align-selections' 57 | 'M': 'envy:right-align-selections' 58 | 59 | # Movement 60 | 'y': 'editor:move-to-beginning-of-previous-paragraph' 61 | 'Y': 'editor:select-to-beginning-of-previous-paragraph' 62 | 'alt-y': 'core:page-up' 63 | 'alt-Y': 'core:select-page-up' 64 | 'h': 'editor:move-to-beginning-of-next-paragraph' 65 | 'H': 'editor:select-to-beginning-of-next-paragraph' 66 | 'alt-h': 'core:page-down' 67 | 'alt-H': 'core:select-page-down' 68 | 69 | 'j': 'core:move-left' 70 | 'J': 'core:select-left' 71 | 'alt-j': 'editor:move-to-previous-subword-boundary' 72 | 'alt-J': 'editor:select-to-previous-subword-boundary' 73 | 'k': 'core:move-down' 74 | 'K': 'core:select-down' 75 | 'alt-k': 'editor:add-selection-below' 76 | 'l': 'core:move-right' 77 | 'L': 'core:select-right' 78 | 'alt-l': 'editor:move-to-next-subword-boundary' 79 | 'alt-L': 'editor:select-to-next-subword-boundary' 80 | 'i': 'core:move-up' 81 | 'I': 'core:select-up' 82 | 'alt-i': 'editor:add-selection-above' 83 | 84 | 'u': 'editor:move-to-beginning-of-line' 85 | 'U': 'editor:select-to-beginning-of-line' 86 | 'o': 'editor:move-to-end-of-line' 87 | 'O': 'editor:select-to-end-of-line' 88 | -------------------------------------------------------------------------------- /lib/base-classes.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | export class EditorContext { 15 | get editor() { 16 | return atom.workspace.getActiveTextEditor(); 17 | } 18 | 19 | isEmptyLine(row) { 20 | return row < 0 || row > this.editor.getLastBufferRow() || this.editor.getBuffer().isRowBlank(row); 21 | } 22 | 23 | getNthMatch(regex, startPosition, n) { 24 | let match = null; 25 | 26 | let i = 0; 27 | 28 | let iterator = ({range, stop}) => { 29 | i++; 30 | if (i === Math.abs(n)) { 31 | match = range; 32 | stop(); 33 | } 34 | }; 35 | 36 | if (n > 0) { 37 | this.editor.scanInBufferRange(regex, [startPosition, [Infinity, Infinity]], iterator); 38 | } else { 39 | this.editor.backwardsScanInBufferRange(regex, [[0, 0], startPosition], iterator); 40 | } 41 | 42 | return match; 43 | } 44 | } 45 | 46 | export class SelectionTransformer extends EditorContext { 47 | transform(selections) { 48 | throw 'Method not implemented!'; 49 | } 50 | } 51 | 52 | export class SingleSelectionTransformer extends SelectionTransformer { 53 | transform(selections) { 54 | let result = []; 55 | for (let i = 0; i < selections.length; i++) { 56 | for (let transformedSelection of this.transformSingle(selections[i], i, selections)) { 57 | result.push(transformedSelection); 58 | } 59 | } 60 | return result; 61 | } 62 | 63 | transformSingle(selection, i, selections) { 64 | throw 'Method not implemented!'; 65 | } 66 | } 67 | 68 | export class MapSelectionTransformer extends SingleSelectionTransformer { 69 | constructor(callback) { 70 | super(); 71 | this.transformSingle = callback; 72 | } 73 | } 74 | 75 | export class SelectionFilter extends MapSelectionTransformer { 76 | constructor(filter) { 77 | super((selection, i, selections) => filter(selection, i, selections) ? [selection] : []); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/block-selection.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | import {SingleSelectionTransformer} from './base-classes'; 15 | import {getTextType} from './utilities'; 16 | 17 | export class BlockSelector extends SingleSelectionTransformer { 18 | transformSingle(selection) { 19 | let start = selection.start; 20 | let end = selection.end; 21 | 22 | if (selection.isSingleLine()) { 23 | let textType = selection.getTextType(); 24 | 25 | if (start.canExpandTextType(textType, -1) || end.canExpandTextType(textType, 1)) { 26 | // Expand to token 27 | while (start.canExpandTextType(textType, -1)) { 28 | start = start.getRelativePosition(-1); 29 | } 30 | while (end.canExpandTextType(textType, 1)) { 31 | end = end.getRelativePosition(1); 32 | } 33 | return [[start, end]]; 34 | 35 | } else { 36 | // Expand to full line 37 | return [[ 38 | [start.row, 0], 39 | [end.row + 1, 0] 40 | ]]; 41 | } 42 | 43 | } else if (selection.isFullLines()) { 44 | // Expand to paragraph 45 | let startRow = start.row; 46 | while (!this.isEmptyLine(startRow - 1)) { 47 | startRow--; 48 | } 49 | let endRow = (end.column === 0) ? end.row - 1 : end.row; 50 | while (!this.isEmptyLine(endRow + 1)) { 51 | endRow++; 52 | } 53 | return [[ 54 | [startRow, 0], 55 | [endRow + 1, 0] 56 | ]]; 57 | 58 | } else { 59 | // Expand to full lines 60 | return [[ 61 | [start.row, 0], 62 | [end.row + 1, 0] 63 | ]]; 64 | } 65 | } 66 | } 67 | 68 | export class BlockSelectionMover extends SingleSelectionTransformer { 69 | constructor(direction, sameTextType) { 70 | super(); 71 | this.direction = direction; 72 | this.sameTextType = sameTextType; 73 | } 74 | 75 | transformSingle(selection) { 76 | let textType = selection.getTextType(); 77 | 78 | if (selection.isFullLine()) { 79 | // Move to next line 80 | return [[ 81 | [selection.start.row + this.direction, 0], 82 | [selection.start.row + this.direction + 1, 0] 83 | ]]; 84 | 85 | } else if (textType >= 0) { 86 | // Move to next token 87 | let start = (this.direction > 0) ? selection.end : selection.start; 88 | while (start.getRelativeTextType(this.direction) === textType) { 89 | start = start.getRelativePosition(this.direction); 90 | } 91 | 92 | let nextTextType = -1; 93 | 94 | if (this.sameTextType) { 95 | // Find start of next token with same type as selected one 96 | while (true) { 97 | let character = start.getRelativeText(this.direction); 98 | if (character.length === 0) { 99 | // End of buffer reached 100 | break; 101 | } 102 | if (getTextType(character) === textType) { 103 | nextTextType = textType; 104 | break; 105 | } 106 | start = start.getRelativePosition(this.direction); 107 | } 108 | } else { 109 | nextTextType = start.getRelativeTextType(this.direction); 110 | } 111 | 112 | if (nextTextType >= 0) { 113 | // There is a next token 114 | let end = start.getRelativePosition(this.direction); 115 | while (end.getRelativeTextType(this.direction) === nextTextType) { 116 | end = end.getRelativePosition(this.direction); 117 | } 118 | return [[start, end]]; 119 | } else { 120 | return [selection]; 121 | } 122 | 123 | } else if (selection.isSingleLine()) { 124 | // Move to next line 125 | return [[ 126 | [selection.start.row + this.direction, 0], 127 | [selection.start.row + this.direction + 1, 0] 128 | ]]; 129 | 130 | } else { 131 | // Move to next paragraph 132 | let startRow = (this.direction > 0) ? 133 | (selection.end.isLineStart() ? selection.end.row : selection.end.row + 1) : 134 | (selection.start.row - 1); 135 | 136 | let buffer = this.editor.getBuffer(); 137 | 138 | while (!(!buffer.isRowBlank(startRow) && buffer.isRowBlank(startRow - this.direction))) { 139 | if (startRow <= 0 || startRow >= this.editor.getLastBufferRow()) { 140 | // End of buffer reached without finding another paragraph 141 | return [selection]; 142 | } 143 | startRow += this.direction; 144 | } 145 | 146 | let endRow = startRow; 147 | 148 | while (!this.isEmptyLine(endRow + this.direction)) { 149 | endRow += this.direction; 150 | } 151 | 152 | return [[ 153 | [Math.min(startRow, endRow), 0], 154 | [Math.max(startRow, endRow) + 1, 0] 155 | ]]; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/bracket-selection.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | import {SingleSelectionTransformer} from './base-classes'; 15 | import {getTextRegex, getBracketBalance} from './utilities'; 16 | 17 | let unpairedBrackets = []; 18 | let pairedBrackets = [ 19 | // XML tag pair 20 | [/<[a-zA-Z](?:[^>]*[^/>])?>/g, /<\/[a-zA-Z]+>/g], 21 | // XML comment 22 | [//g], 23 | // C-style comment 24 | [/\/\*/g, /\*\//g] 25 | ]; 26 | 27 | const autocompleteCharacters = atom.config.get('bracket-matcher.autocompleteCharacters'); 28 | 29 | if (typeof autocompleteCharacters !== 'undefined') { 30 | for (let characterPair of autocompleteCharacters.map(s => Array.from(s))) { 31 | if (characterPair[0] === characterPair[1]) { 32 | unpairedBrackets.push(getTextRegex(characterPair[0])); 33 | } else { 34 | pairedBrackets.push(characterPair.map(getTextRegex)); 35 | } 36 | } 37 | } 38 | 39 | export class BracketSelector extends SingleSelectionTransformer { 40 | transformSingle(selection) { 41 | let matches = []; 42 | 43 | for (let bracket of unpairedBrackets) { 44 | // Only expand to unpaired brackets of types not contained in the selection 45 | if (selection.getMatchCount(bracket) === 0) { 46 | let startRange = this.getNthMatch(bracket, selection.start, -1); 47 | let endRange = this.getNthMatch(bracket, selection.end, 1); 48 | 49 | // Only accept single-line ranges for unpaired brackets 50 | // to avoid matching unrelated brackets across the buffer 51 | if (startRange !== null && endRange !== null && startRange.end.row === endRange.start.row) { 52 | matches.push({ 53 | startRange: startRange, 54 | endRange: endRange 55 | }); 56 | } 57 | } 58 | } 59 | 60 | for (let bracketPair of pairedBrackets) { 61 | let startRange = this.getNthMatch(bracketPair[0], selection.start, -1); 62 | let endRange = this.getNthMatch(bracketPair[1], selection.end, 1); 63 | 64 | while (startRange !== null && endRange !== null) { 65 | let bracketText = this.editor.getTextInBufferRange([startRange.end, endRange.start]); 66 | let bracketBalance = getBracketBalance(bracketPair, bracketText); 67 | 68 | if (bracketBalance[0] === 0 && bracketBalance[1] === 0) 69 | break; 70 | 71 | if (bracketBalance[1] > 0) 72 | startRange = this.getNthMatch(bracketPair[0], startRange.start, -bracketBalance[1]); 73 | 74 | if (bracketBalance[0] > 0) 75 | endRange = this.getNthMatch(bracketPair[1], endRange.end, bracketBalance[0]); 76 | } 77 | 78 | if (startRange !== null && endRange !== null) { 79 | matches.push({ 80 | startRange: startRange, 81 | endRange: endRange 82 | }); 83 | } 84 | } 85 | 86 | if (matches.length > 0) { 87 | for (let match of matches) { 88 | match.score = this.editor.getTextInBufferRange([match.startRange.end, match.endRange.start]).length; 89 | } 90 | 91 | let bestMatch = matches.sort((a, b) => a.score - b.score)[0]; 92 | 93 | if (bestMatch.startRange.end.isEqual(selection.start) && bestMatch.endRange.start.isEqual(selection.end)) { 94 | // Selection completely covers the range enclosed by the brackets => expand to outside of brackets 95 | return [[bestMatch.startRange.start, bestMatch.endRange.end]]; 96 | } else { 97 | // Expand to inside of brackets 98 | return [[bestMatch.startRange.end, bestMatch.endRange.start]]; 99 | } 100 | 101 | } else { 102 | return [selection]; 103 | } 104 | } 105 | } 106 | 107 | export class BracketSelectionMover extends SingleSelectionTransformer { 108 | constructor(direction) { 109 | super(); 110 | this.direction = direction; 111 | } 112 | 113 | transformSingle(selection) { 114 | let matches = []; 115 | 116 | for (let bracket of unpairedBrackets) { 117 | let startPosition = (this.direction > 0) ? selection.end : selection.start; 118 | 119 | let startRange = null; 120 | let endRange = null; 121 | 122 | while (true) { 123 | startRange = this.getNthMatch(bracket, startPosition, this.direction); 124 | if (startRange === null) 125 | break; 126 | 127 | startPosition = (this.direction > 0) ? startRange.end : startRange.start; 128 | endRange = this.getNthMatch(bracket, startPosition, this.direction); 129 | if (endRange === null) 130 | break; 131 | 132 | if (startRange.end.row === endRange.start.row) 133 | break; 134 | } 135 | 136 | if (startRange !== null && endRange !== null) { 137 | matches.push({ 138 | startRange: (this.direction > 0) ? startRange : endRange, 139 | endRange: (this.direction > 0) ? endRange : startRange 140 | }); 141 | } 142 | } 143 | 144 | for (let bracketPair of pairedBrackets) { 145 | let startPosition = (this.direction > 0) ? selection.end : selection.start; 146 | let startRange = this.getNthMatch(bracketPair[(this.direction > 0) ? 0 : 1], startPosition, this.direction); 147 | 148 | if (startRange !== null) { 149 | startPosition = (this.direction > 0) ? startRange.end : startRange.start; 150 | let endRange = this.getNthMatch(bracketPair[(this.direction > 0) ? 1 : 0], startPosition, this.direction); 151 | 152 | while (endRange !== null) { 153 | let bracketText = this.editor.getTextInBufferRange((this.direction > 0) ? 154 | [startRange.end, endRange.start] : 155 | [endRange.end, startRange.start]); 156 | let bracketBalance = getBracketBalance(bracketPair, bracketText); 157 | 158 | if (bracketBalance[0] === 0 && bracketBalance[1] === 0) 159 | break; 160 | 161 | if (bracketBalance[0] > 0 && this.direction > 0) { 162 | endRange = this.getNthMatch(bracketPair[1], endRange.end, bracketBalance[0]); 163 | } else if (bracketBalance[1] > 0 && this.direction < 0) { 164 | endRange = this.getNthMatch(bracketPair[0], endRange.start, -bracketBalance[1]); 165 | } else { 166 | throw 'Unexpected bracket balance!'; 167 | } 168 | } 169 | 170 | if (endRange !== null) { 171 | matches.push({ 172 | startRange: (this.direction > 0) ? startRange : endRange, 173 | endRange: (this.direction > 0) ? endRange : startRange 174 | }); 175 | } 176 | } 177 | } 178 | 179 | if (matches.length > 0) { 180 | for (let match of matches) { 181 | let distanceRange = (this.direction > 0) ? 182 | [selection.end, match.startRange.start] : 183 | [selection.start, match.endRange.end]; 184 | match.score = this.editor.getTextInBufferRange(distanceRange).length; 185 | } 186 | let bestMatch = matches.sort((a, b) => a.score - b.score)[0]; 187 | return [[bestMatch.startRange.start, bestMatch.endRange.end]]; 188 | } else { 189 | return [selection]; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /lib/clipboard.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | import {EditorContext} from './base-classes'; 15 | 16 | export default class Clipboard extends EditorContext { 17 | constructor() { 18 | super(); 19 | this.data = []; 20 | } 21 | 22 | copy() { 23 | this.data = []; 24 | 25 | // https://github.com/atom/atom/blob/v1.14.4/src/text-editor.coffee#L3108 26 | for (let selection of this.editor.getSelectionsOrderedByBufferPosition()) { 27 | if (selection.isEmpty()) { 28 | // Copy line 29 | let range = selection.getBufferRange(); 30 | selection.selectLine(); 31 | this.data.push(selection.getText()); 32 | selection.setBufferRange(range); 33 | } else { 34 | this.data.push(selection.getText()); 35 | } 36 | } 37 | } 38 | 39 | cut() { 40 | this.copy(); 41 | 42 | this.editor.mutateSelectedText(selection => { 43 | if (selection.isEmpty()) { 44 | selection.deleteLine(); 45 | } else { 46 | selection.delete(); 47 | } 48 | }); 49 | } 50 | 51 | paste() { 52 | let corresponding = (this.data.length === this.editor.getSelections().length); 53 | let joinedData = this.data.join('\n'); 54 | 55 | this.editor.mutateSelectedText((selection, i) => { 56 | selection.insertText(corresponding ? this.data[i] : joinedData); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/extent.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | import {Range} from 'atom'; 15 | 16 | import {EditorContext} from './base-classes'; 17 | import {getTextType} from './utilities'; 18 | import Position from './position'; 19 | 20 | // An editor-aware "Range" alternative 21 | export default class Extent extends EditorContext { 22 | constructor(start, end) { 23 | super(); 24 | this.start = new Position(start[0], start[1]); 25 | this.end = new Position(end[0], end[1]); 26 | } 27 | 28 | static fromRange(range) { 29 | range = Range.fromObject(range); 30 | return new Extent([range.start.row, range.start.column], [range.end.row, range.end.column]); 31 | } 32 | 33 | toRange() { 34 | return new Range(this.start, this.end); 35 | } 36 | 37 | isEqual(extent) { 38 | return this.toRange().isEqual(extent); 39 | } 40 | 41 | isEmpty() { 42 | return this.toRange().isEmpty(); 43 | } 44 | 45 | isSingleLine() { 46 | return this.toRange().isSingleLine(); 47 | } 48 | 49 | isFullLine() { 50 | return (this.start.isLineStart() && this.end.isLineEnd() && this.isSingleLine()) || 51 | (this.start.isLineStart() && this.end.isLineStart() && this.end.row === this.start.row + 1); 52 | } 53 | 54 | isFullLines() { 55 | return (this.start.isLineStart() && this.end.isLineEnd()) || 56 | (this.start.isLineStart() && this.end.isLineStart() && !this.isSingleLine()); 57 | } 58 | 59 | getText() { 60 | return this.editor.getTextInBufferRange(this); 61 | } 62 | 63 | getTextType() { 64 | if (this.isEmpty()) { 65 | // Choose the higher priority type 66 | return Math.max(this.start.getRelativeTextType(-1), this.end.getRelativeTextType(1)); 67 | } else { 68 | return getTextType(this.getText()); 69 | } 70 | } 71 | 72 | getMatchCount(regex) { 73 | // http://stackoverflow.com/q/1072765#comment9785878_1072782 74 | return (this.getText().match(regex) || []).length; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | import {CompositeDisposable} from 'atom'; 15 | 16 | import Clipboard from './clipboard'; 17 | import Extent from './extent'; 18 | import {SelectionTransformer, MapSelectionTransformer, SelectionFilter} from './base-classes'; 19 | import SelectionInverter from './selection-inverter'; 20 | import {BracketSelector, BracketSelectionMover} from './bracket-selection'; 21 | import {BlockSelector, BlockSelectionMover} from './block-selection'; 22 | import {MatchSelector, MatchSelectionMover} from './match-selection'; 23 | 24 | export default { 25 | subscriptions: null, 26 | 27 | secondaryClipboard: new Clipboard(), 28 | 29 | activate(state) { 30 | let commands = { 31 | 'envy:toggle': function() { 32 | this.classList.toggle('envy-mode'); 33 | }, 34 | 35 | 'envy:select-all-brackets': () => this.transformSelectionsCumulative( 36 | [new BracketSelectionMover(-1), new BracketSelectionMover(1)]), 37 | 'envy:select-all-blocks': () => this.transformSelectionsCumulative( 38 | [new BlockSelectionMover(-1, true), new BlockSelectionMover(1, true)]), 39 | 40 | 'envy:copy-to-secondary-clipboard': () => this.secondaryClipboard.copy(), 41 | 'envy:paste-from-secondary-clipboard': () => this.secondaryClipboard.paste(), 42 | 'envy:cut-to-secondary-clipboard': () => this.secondaryClipboard.cut(), 43 | 44 | 'envy:swap-selections': () => this.replaceSelections((i, selectionTexts) => { 45 | // Replace with text of other selection in pair (1 <-> 2, 3 <-> 4 etc.) 46 | if (i % 2 === 1) { 47 | return selectionTexts[i - 1]; 48 | } else if (i < selectionTexts.length - 1) { 49 | return selectionTexts[i + 1]; 50 | } else { 51 | return null; 52 | } 53 | }), 54 | 'envy:swap-selections-alternative': () => this.replaceSelections((i, selectionTexts) => { 55 | // Replace with text of other selection in pair (1 <-> 3, 2 <-> 4 etc.) 56 | if (i % 4 >= 2) { 57 | return selectionTexts[i - 2]; 58 | } else if (i < selectionTexts.length - 2) { 59 | return selectionTexts[i + 2]; 60 | } else { 61 | return null; 62 | } 63 | }), 64 | 65 | 'envy:rotate-selections-forward': () => this.replaceSelections( 66 | (i, selectionTexts) => selectionTexts[(i - 1 + selectionTexts.length) % selectionTexts.length]), 67 | 'envy:rotate-selections-backward': () => this.replaceSelections( 68 | (i, selectionTexts) => selectionTexts[(i + 1) % selectionTexts.length]), 69 | 70 | 'envy:left-align-selections': () => this.alignSelections(false), 71 | 'envy:right-align-selections': () => this.alignSelections(true) 72 | }; 73 | 74 | let selectionTransformerCommands = [ 75 | ['remove-every-second-selection-even', new SelectionFilter((s, i) => i % 2 === 0), false], 76 | ['remove-every-second-selection-odd', new SelectionFilter((s, i) => i % 2 === 1), false], 77 | 78 | ['split-selections-into-cursors', new MapSelectionTransformer(s => [[s.start, s.start], [s.end, s.end]]), false], 79 | 80 | ['invert-selections-in-lines', new SelectionInverter(true), false], 81 | ['invert-selections', new SelectionInverter(false), false], 82 | 83 | ['select-surrounding-brackets', new BracketSelector(), false], 84 | ['move-bracket-selection-backward', new BracketSelectionMover(-1), false], 85 | ['add-bracket-selection-backward', new BracketSelectionMover(-1), true], 86 | ['move-bracket-selection-forward', new BracketSelectionMover(1), false], 87 | ['add-bracket-selection-forward', new BracketSelectionMover(1), true], 88 | 89 | ['select-surrounding-block', new BlockSelector(), false], 90 | ['move-block-selection-backward', new BlockSelectionMover(-1, true), false], 91 | ['add-block-selection-backward', new BlockSelectionMover(-1, true), true], 92 | ['move-block-selection-backward-alternative', new BlockSelectionMover(-1, false), false], 93 | ['add-block-selection-backward-alternative', new BlockSelectionMover(-1, false), true], 94 | ['move-block-selection-forward', new BlockSelectionMover(1, true), false], 95 | ['add-block-selection-forward', new BlockSelectionMover(1, true), true], 96 | ['move-block-selection-forward-alternative', new BlockSelectionMover(1, false), false], 97 | ['add-block-selection-forward-alternative', new BlockSelectionMover(1, false), true], 98 | 99 | ['select-all-matches-in-lines', new MatchSelector(true), false], 100 | ['select-all-matches', new MatchSelector(false), false], 101 | ['select-previous-match', new MatchSelectionMover(-1), false], 102 | ['add-select-previous-match', new MatchSelectionMover(-1), true], 103 | ['select-next-match', new MatchSelectionMover(1), false], 104 | ['add-select-next-match', new MatchSelectionMover(1), true] 105 | ]; 106 | 107 | for (let command of selectionTransformerCommands) { 108 | commands['envy:' + command[0]] = () => this.transformSelections(command[1], command[2]); 109 | } 110 | 111 | this.subscriptions = new CompositeDisposable(); 112 | this.subscriptions.add(atom.commands.add('atom-text-editor', commands)); 113 | }, 114 | 115 | deactivate() { 116 | this.subscriptions.dispose(); 117 | }, 118 | 119 | transformSelections(transformer, addToExistingSelections) { 120 | if (!(transformer instanceof SelectionTransformer)) 121 | throw 'transformer must be an instance of SelectionTransformer!'; 122 | 123 | let editor = atom.workspace.getActiveTextEditor(); 124 | 125 | let selections = editor.getSelectionsOrderedByBufferPosition().map(s => Extent.fromRange(s.getBufferRange())); 126 | 127 | let transformedSelections = transformer.transform(selections); 128 | 129 | if (addToExistingSelections) { 130 | for (let selection of transformedSelections) { 131 | selections.push(selection); 132 | } 133 | } else { 134 | selections = transformedSelections; 135 | } 136 | 137 | if (selections.length > 0) 138 | editor.setSelectedBufferRanges(selections); 139 | }, 140 | 141 | transformSelectionsCumulative(transformers) { 142 | let buffer = atom.workspace.getActiveTextEditor().getBuffer(); 143 | 144 | this.transformSelections(new MapSelectionTransformer(selection => { 145 | let result = []; 146 | 147 | for (let transformer of transformers) { 148 | let currentSelection = selection; 149 | 150 | while (true) { 151 | let nextSelection = transformer.transform([currentSelection])[0]; 152 | nextSelection = buffer.clipRange(nextSelection); 153 | nextSelection = Extent.fromRange(nextSelection); 154 | 155 | if (nextSelection.isEqual(currentSelection)) 156 | break; 157 | 158 | result.push(nextSelection); 159 | currentSelection = nextSelection; 160 | } 161 | } 162 | 163 | return result; 164 | }), true); 165 | }, 166 | 167 | replaceSelections(callback) { 168 | let editor = atom.workspace.getActiveTextEditor(); 169 | 170 | let selectionTexts = editor.getSelectionsOrderedByBufferPosition().map(selection => selection.getText()); 171 | 172 | editor.mutateSelectedText((selection, i) => { 173 | let replacementText = callback(i, selectionTexts); 174 | 175 | if (replacementText !== null) { 176 | let range = selection.getBufferRange(); 177 | selection.insertText(replacementText); 178 | selection.setBufferRange([range.start, selection.getBufferRange().end]); 179 | } 180 | }); 181 | }, 182 | 183 | alignSelections(alignRight) { 184 | let editor = atom.workspace.getActiveTextEditor(); 185 | 186 | let selections = editor.getSelectionsOrderedByBufferPosition(); 187 | 188 | let binnedSelections = []; 189 | 190 | for (let i = 0; i < selections.length; i++) { 191 | let row = selections[i].getBufferRange().start.row; 192 | 193 | // bin = index of selection among selections on the same line 194 | let bin = 0; 195 | for (let j = i - 1; j >= 0; j--) { 196 | if (selections[j].getBufferRange().start.row !== row) 197 | break; 198 | bin++; 199 | } 200 | 201 | if (!(bin in binnedSelections)) 202 | binnedSelections[bin] = []; 203 | 204 | binnedSelections[bin].push(selections[i]); 205 | } 206 | 207 | editor.transact(() => { 208 | for (let binSelections of binnedSelections) { 209 | let maxColumn = 0; 210 | 211 | for (let selection of binSelections) { 212 | let range = selection.getBufferRange(); 213 | maxColumn = Math.max(maxColumn, alignRight ? range.end.column : range.start.column); 214 | } 215 | 216 | for (let selection of binSelections) { 217 | let range = selection.getBufferRange(); 218 | let start = range.start; 219 | let padding = ' '.repeat(maxColumn - (alignRight ? range.end.column : start.column)); 220 | editor.setTextInBufferRange([start, start], padding); 221 | selection.setBufferRange([[start.row, start.column + padding.length], selection.getBufferRange().end]); 222 | } 223 | } 224 | }); 225 | } 226 | }; 227 | -------------------------------------------------------------------------------- /lib/match-selection.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | import {SingleSelectionTransformer} from './base-classes'; 15 | import {getTextRegex} from './utilities'; 16 | 17 | export class MatchSelector extends SingleSelectionTransformer { 18 | constructor(selectInLines) { 19 | super(); 20 | this.selectInLines = selectInLines; 21 | } 22 | 23 | transformSingle(selection) { 24 | if (selection.isEmpty()) 25 | return [selection]; 26 | 27 | let result = []; 28 | 29 | let regex = getTextRegex(selection.getText()); 30 | let iterator = ({range}) => result.push(range); 31 | 32 | if (this.selectInLines) { 33 | this.editor.scanInBufferRange(regex, [[selection.start.row, 0], [selection.end.row, Infinity]], iterator); 34 | } else { 35 | this.editor.scan(regex, iterator); 36 | } 37 | 38 | return result; 39 | } 40 | } 41 | 42 | export class MatchSelectionMover extends SingleSelectionTransformer { 43 | constructor(direction) { 44 | super(); 45 | this.direction = direction; 46 | } 47 | 48 | transformSingle(selection) { 49 | if (selection.isEmpty()) 50 | return [selection]; 51 | 52 | let regex = getTextRegex(selection.getText()); 53 | let startPosition = (this.direction > 0) ? selection.end : selection.start; 54 | 55 | let match = this.getNthMatch(regex, startPosition, this.direction); 56 | 57 | return [(match === null) ? selection : match]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/position.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | import {Point} from 'atom'; 15 | 16 | import {EditorContext} from './base-classes'; 17 | import {getTextType} from './utilities'; 18 | 19 | // An editor-aware "Point" alternative 20 | export default class Position extends EditorContext { 21 | constructor(row, column) { 22 | super(); 23 | this.row = row; 24 | this.column = column; 25 | } 26 | 27 | static fromPoint(point) { 28 | point = Point.fromObject(point); 29 | return new Position(point.row, point.column); 30 | } 31 | 32 | toPoint() { 33 | return new Point(this.row, this.column); 34 | } 35 | 36 | isEqual(position) { 37 | return this.toPoint().isEqual(position); 38 | } 39 | 40 | copy(row = this.row, column = this.column) { 41 | return new Position(row, column); 42 | } 43 | 44 | setClipped(row, column) { 45 | let point = this.editor.clipBufferPosition([row, column]); 46 | this.row = point.row; 47 | this.column = point.column; 48 | } 49 | 50 | isBufferStart() { 51 | return this.isEqual([0, 0]); 52 | } 53 | 54 | isBufferEnd() { 55 | return this.isEqual(this.editor.getBuffer().getEndPosition()); 56 | } 57 | 58 | isLineStart() { 59 | return this.column === 0; 60 | } 61 | 62 | isLineEnd() { 63 | return this.column === this.editor.getBuffer().lineLengthForRow(this.row); 64 | } 65 | 66 | getRelativePosition(offset) { 67 | let position = this.copy(); 68 | 69 | for (let i = 0; i < Math.abs(offset); i++) { 70 | if (offset > 0) { 71 | if (position.isLineEnd()) { 72 | position.setClipped(position.row + 1, 0); 73 | } else { 74 | position.column++; 75 | } 76 | } else { 77 | if (position.isLineStart()) { 78 | position.setClipped(position.row - 1, Infinity); 79 | } else { 80 | position.column--; 81 | } 82 | } 83 | } 84 | 85 | return position; 86 | } 87 | 88 | getRelativeText(offset) { 89 | return this.editor.getTextInBufferRange([this, this.getRelativePosition(offset)]); 90 | } 91 | 92 | getRelativeTextType(offset) { 93 | return getTextType(this.getRelativeText(offset)); 94 | } 95 | 96 | canExpandTextType(textType, direction) { 97 | return textType >= 0 && this.getRelativeTextType(direction) === textType && 98 | !((direction > 0) ? this.isLineEnd() : this.isLineStart()); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/selection-inverter.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | import {SelectionTransformer} from './base-classes'; 15 | 16 | export default class SelectionInverter extends SelectionTransformer { 17 | constructor(invertInLines) { 18 | super(); 19 | this.invertInLines = invertInLines; 20 | } 21 | 22 | transform(selections) { 23 | let result = []; 24 | 25 | let selectionsStart = selections[0].start; 26 | let selectionsEnd = selections[selections.length - 1].end; 27 | 28 | if (this.invertInLines) { 29 | if (!selectionsStart.isLineStart()) 30 | result.push([[selectionsStart.row, 0], selectionsStart]); 31 | 32 | for (let i = 0; i < selections.length - 1; i++) { 33 | let start = selections[i].end; 34 | let end = selections[i + 1].start; 35 | 36 | if (start.row === end.row) { 37 | result.push([start, end]); 38 | } else { 39 | if (!start.isLineEnd() && !(start.isLineStart() && !selections[i].isSingleLine())) 40 | result.push([start, [start.row, Infinity]]); 41 | if (!end.isLineStart()) 42 | result.push([[end.row, 0], end]); 43 | } 44 | } 45 | 46 | if (!selectionsEnd.isLineEnd() && 47 | !(selectionsEnd.isLineStart() && !selections[selections.length - 1].isSingleLine())) 48 | result.push([selectionsEnd, [selectionsEnd.row, Infinity]]); 49 | 50 | // Full line selections are dropped by the above code and have no unambiguous inverse, 51 | // so they are added back unchanged here to preserve the involution property of the operation 52 | for (let selection of selections) { 53 | if (selection.isFullLines()) { 54 | result.push(selection); 55 | } 56 | } 57 | 58 | } else { 59 | if (!selectionsStart.isBufferStart()) 60 | result.push([[0, 0], selectionsStart]); 61 | 62 | for (let i = 0; i < selections.length - 1; i++) { 63 | result.push([selections[i].end, selections[i + 1].start]); 64 | } 65 | 66 | if (!selectionsEnd.isBufferEnd()) 67 | result.push([selectionsEnd, [Infinity, Infinity]]); 68 | } 69 | 70 | return result; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/utilities.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | import _ from 'underscore-plus'; 15 | 16 | const nonWordCharacters = atom.config.get('editor.nonWordCharacters'); 17 | 18 | const wordRegex = new RegExp('^[^\\s' + _.escapeRegExp(nonWordCharacters) + ']+$'); 19 | const nonWordRegex = new RegExp('^[' + _.escapeRegExp(nonWordCharacters) + ']+$'); 20 | const whitespaceRegex = /^\s+$/; 21 | 22 | export function getTextType(text) { 23 | if (text.length === 0) { 24 | return -1; 25 | } else if (wordRegex.test(text)) { 26 | return 2; 27 | } else if (nonWordRegex.test(text)) { 28 | return 1; 29 | } else if (whitespaceRegex.test(text)) { 30 | return 0; 31 | } else { 32 | // Mixed 33 | return -1; 34 | } 35 | } 36 | 37 | export function getTextRegex(text) { 38 | return new RegExp(_.escapeRegExp(text), 'g'); 39 | } 40 | 41 | // Return value: [unmatched opening brackets, unmatched closing brackets] 42 | export function getBracketBalance(bracketPair, text) { 43 | // Matches either half of the pair 44 | let pairRegex = new RegExp(bracketPair[0].source + '|' + bracketPair[1].source, 'g'); 45 | 46 | let openingBrackets = 0; 47 | let closingBrackets = 0; 48 | 49 | for (let match of (text.match(pairRegex) || [])) { 50 | if (match.match(bracketPair[0]) !== null) { 51 | // Opening bracket 52 | openingBrackets++; 53 | } else if (match.match(bracketPair[1]) !== null) { 54 | // Closing bracket 55 | if (openingBrackets > 0) { 56 | openingBrackets--; 57 | } else { 58 | closingBrackets++; 59 | } 60 | } else { 61 | throw 'Unexpected match!'; 62 | } 63 | } 64 | 65 | return [openingBrackets, closingBrackets]; 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "envy", 3 | "main": "./lib/main", 4 | "version": "0.2.2", 5 | "description": "Text editing supercharger", 6 | "keywords": [ 7 | "vim", 8 | "keyboard-layout", 9 | "keymap" 10 | ], 11 | "repository": "https://github.com/p-e-w/envy", 12 | "license": "MIT", 13 | "engines": { 14 | "atom": ">=1.0.0 <2.0.0" 15 | }, 16 | "dependencies": { 17 | "underscore-plus": "^1.6.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/envy-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* 4 | * Envy - Text editing supercharger 5 | * 6 | * Copyright (c) 2017 Philipp Emanuel Weidmann 7 | * 8 | * Nemo vir est qui mundum non reddat meliorem. 9 | * 10 | * Released under the terms of the MIT License 11 | * (https://opensource.org/licenses/MIT) 12 | */ 13 | 14 | import fs from 'fs'; 15 | import path from 'path'; 16 | 17 | // Return value: [text without selection annotations, selection ranges] 18 | function extractSelections(text) { 19 | let regex = /<\|([\s\S]*?)\|>/g; 20 | 21 | let selections = []; 22 | 23 | let match; 24 | let i = 0; 25 | 26 | while ((match = regex.exec(text)) !== null) { 27 | // For every preceding selection annotation, 4 characters are removed 28 | let start = match.index - (4 * i); 29 | selections.push([start, start + match[1].length]); 30 | i++; 31 | } 32 | 33 | return [text.replace(regex, (match, p1) => p1), selections]; 34 | } 35 | 36 | describe('Envy', () => { 37 | let editor; 38 | let editorElement; 39 | 40 | beforeEach(() => { 41 | // The bracket-matcher package contains the bracket character pairs used by Envy 42 | waitsForPromise(() => atom.packages.activatePackage('bracket-matcher')); 43 | 44 | waitsForPromise(() => atom.packages.activatePackage('envy')); 45 | 46 | waitsForPromise(() => atom.workspace.open().then(e => { 47 | editor = e; 48 | editorElement = atom.views.getView(e); 49 | })); 50 | }); 51 | 52 | describe('when the envy:toggle command is executed', () => { 53 | it('toggles the envy-mode class on the editor', () => { 54 | expect(editorElement.classList.contains('envy-mode')).toBe(false); 55 | atom.commands.dispatch(editorElement, 'envy:toggle'); 56 | expect(editorElement.classList.contains('envy-mode')).toBe(true); 57 | atom.commands.dispatch(editorElement, 'envy:toggle'); 58 | expect(editorElement.classList.contains('envy-mode')).toBe(false); 59 | }); 60 | }); 61 | 62 | let testsDir = path.join(__dirname, 'tests'); 63 | 64 | for (let file of fs.readdirSync(testsDir)) { 65 | let filePath = path.join(testsDir, file); 66 | 67 | if (path.extname(file) === '.test' && fs.lstatSync(filePath).isFile()) { 68 | describe(file, () => { 69 | let contents = fs.readFileSync(filePath, {encoding: 'utf8'}); 70 | 71 | let parts = contents.split('\n---\n'); 72 | 73 | for (let i = 0; i < parts.length - 3; i += 3) { 74 | let [before, description, commands, after] = parts.slice(i, i + 4); 75 | 76 | it(description, () => { 77 | let [text, selections] = extractSelections(before); 78 | editor.setText(text); 79 | editor.setSelectedBufferRanges(selections.map(selection => { 80 | return selection.map(index => editor.getBuffer().positionForCharacterIndex(index)); 81 | })); 82 | 83 | for (let command of commands.split('\n')) { 84 | atom.commands.dispatch(editorElement, command); 85 | } 86 | 87 | [text, selections] = extractSelections(after); 88 | let actualSelections = editor.getSelectionsOrderedByBufferPosition().map(selection => { 89 | let range = selection.getBufferRange(); 90 | return [range.start, range.end].map(point => editor.getBuffer().characterIndexForPosition(point)); 91 | }); 92 | expect(editor.getText()).toBe(text); 93 | expect(actualSelections).toEqual(selections); 94 | }); 95 | } 96 | }); 97 | } 98 | } 99 | }); 100 | -------------------------------------------------------------------------------- /spec/tests/align-selections.test: -------------------------------------------------------------------------------- 1 | <|abcd|> efg<||> <|hijkl|> mnop 2 | qrs<| t|>uvwx <|yz.|> 3 | 1<||>2<|3 4567|> <|8|>90 4 | --- 5 | left-aligns selections in columns by inserting spaces 6 | --- 7 | envy:left-align-selections 8 | --- 9 | <|abcd|> efg<||> <|hijkl|> mnop 10 | qrs<| t|>uvwx <|yz.|> 11 | 1 <||>2 <|3 4567|> <|8|>90 12 | --- 13 | does nothing when the selections are already aligned 14 | --- 15 | envy:left-align-selections 16 | --- 17 | <|abcd|> efg<||> <|hijkl|> mnop 18 | qrs<| t|>uvwx <|yz.|> 19 | 1 <||>2 <|3 4567|> <|8|>90 20 | --- 21 | right-aligns selections in columns by inserting spaces 22 | --- 23 | envy:right-align-selections 24 | --- 25 | <|abcd|> efg <||> <|hijkl|> mnop 26 | qrs <| t|>uvwx <|yz.|> 27 | 1 <||>2 <|3 4567|> <|8|>90 28 | --- 29 | does nothing when the selections are already aligned 30 | --- 31 | envy:right-align-selections 32 | --- 33 | <|abcd|> efg <||> <|hijkl|> mnop 34 | qrs <| t|>uvwx <|yz.|> 35 | 1 <||>2 <|3 4567|> <|8|>90 36 | --- 37 | -------------------------------------------------------------------------------- /spec/tests/block-selection-1.test: -------------------------------------------------------------------------------- 1 | /*<| 2 | Comm|>ent 3 | */ 4 | 5 | class<||> A { 6 | 7 | b(c) { 8 | <| |> d = "<||>abcdefg"; 9 | e = [1, 2, 3, 4, 5];<||> 10 | } 11 | 12 | } 13 | --- 14 | expands multi-line selections to full lines and selections within words/non-words/whitespace to full tokens 15 | --- 16 | envy:select-surrounding-block 17 | --- 18 | <|/* 19 | Comment 20 | |>*/ 21 | 22 | <|class|> A { 23 | 24 | b(c) { 25 | <| |>d = "<|abcdefg|>"; 26 | e = [1, 2, 3, 4, 5<|];|> 27 | } 28 | 29 | } 30 | --- 31 | expands full line selections to paragraphs and full token selections to full lines 32 | --- 33 | envy:select-surrounding-block 34 | --- 35 | <|/* 36 | Comment 37 | */ 38 | |> 39 | <|class A { 40 | |> 41 | b(c) { 42 | <| d = "abcdefg"; 43 | |><| e = [1, 2, 3, 4, 5]; 44 | |> } 45 | 46 | } 47 | --- 48 | expands full line selections to paragraphs 49 | --- 50 | envy:select-surrounding-block 51 | --- 52 | <|/* 53 | Comment 54 | */ 55 | |> 56 | <|class A { 57 | |> 58 | <| b(c) { 59 | d = "abcdefg"; 60 | e = [1, 2, 3, 4, 5]; 61 | } 62 | |> 63 | } 64 | --- 65 | -------------------------------------------------------------------------------- /spec/tests/block-selection-2.test: -------------------------------------------------------------------------------- 1 | /* Comment */ 2 | 3 | class <|A|> { 4 | 5 | b(c) { 6 | d = "abcdefg"; 7 | e = [1, 2, 3, 4, 5]; 8 | } 9 | 10 | } 11 | --- 12 | selects all words 13 | --- 14 | envy:select-all-blocks 15 | --- 16 | /* <|Comment|> */ 17 | 18 | <|class|> <|A|> { 19 | 20 | <|b|>(<|c|>) { 21 | <|d|> = "<|abcdefg|>"; 22 | <|e|> = [<|1|>, <|2|>, <|3|>, <|4|>, <|5|>]; 23 | } 24 | 25 | } 26 | --- 27 | -------------------------------------------------------------------------------- /spec/tests/block-selection-3.test: -------------------------------------------------------------------------------- 1 | /* <||>Comment<| |><|*/|> 2 | class A { 3 | <| 4 | |><| b(c) { 5 | d = "abcdefg"; 6 | e = [1, 2, 3, 4, 5]; 7 | }|> 8 | 9 | } 10 | --- 11 | moves each selection to the next block of the same type as the selection 12 | --- 13 | envy:move-block-selection-forward 14 | --- 15 | /* Comment */<| 16 | |><|class|> A <|{|> 17 | 18 | <| b(c) { 19 | |> d = "abcdefg"; 20 | e = [1, 2, 3, 4, 5]; 21 | } 22 | 23 | <|}|> 24 | --- 25 | moves each selection to the previous block of the same type as the selection 26 | --- 27 | envy:move-block-selection-backward 28 | --- 29 | /* <|Comment|><| |><|*/|> 30 | class A { 31 | <| 32 | |> b(c) { 33 | d = "abcdefg"; 34 | e = [1, 2, 3, 4, 5]; 35 | } 36 | <| 37 | |>} 38 | --- 39 | moves each selection to the next block of the same type as the selection, ignoring token type 40 | --- 41 | envy:move-block-selection-forward-alternative 42 | --- 43 | /* Comment<| |><|*/|><| 44 | |>class A { 45 | 46 | <| b(c) { 47 | |> d = "abcdefg"; 48 | e = [1, 2, 3, 4, 5]; 49 | } 50 | 51 | <|}|> 52 | --- 53 | moves each selection to the previous block of the same type as the selection, ignoring token type 54 | --- 55 | envy:move-block-selection-backward-alternative 56 | --- 57 | /* <|Comment|><| |><|*/|> 58 | class A { 59 | <| 60 | |> b(c) { 61 | d = "abcdefg"; 62 | e = [1, 2, 3, 4, 5]; 63 | } 64 | <| 65 | |>} 66 | --- 67 | adds the next block of the same type as the selection to each selection 68 | --- 69 | envy:add-block-selection-forward 70 | --- 71 | /* <|Comment|><| |><|*/|><| 72 | |><|class|> A <|{|> 73 | <| 74 | |><| b(c) { 75 | |> d = "abcdefg"; 76 | e = [1, 2, 3, 4, 5]; 77 | } 78 | <| 79 | |><|}|> 80 | --- 81 | adds the previous block of the same type as the selection to each selection 82 | --- 83 | envy:add-block-selection-backward 84 | --- 85 | <|/*|><| |><|Comment|><| |><|*/|><| 86 | |><|class A { 87 | |><| 88 | |><| b(c) { 89 | |> d = "abcdefg"; 90 | e = [1, 2, 3, 4, 5]; 91 | <| } 92 | |><| 93 | |><|}|> 94 | --- 95 | prepares the selections for the next test 96 | --- 97 | editor:consolidate-selections 98 | envy:move-block-selection-forward-alternative 99 | envy:move-block-selection-forward-alternative 100 | envy:add-block-selection-forward 101 | --- 102 | /* <|Comment|> */ 103 | <|class|> A { 104 | 105 | b(c) { 106 | d = "abcdefg"; 107 | e = [1, 2, 3, 4, 5]; 108 | } 109 | 110 | } 111 | --- 112 | adds the next block of the same type as the selection to each selection, ignoring token type 113 | --- 114 | envy:add-block-selection-forward-alternative 115 | --- 116 | /* <|Comment|><| |>*/ 117 | <|class|><| |>A { 118 | 119 | b(c) { 120 | d = "abcdefg"; 121 | e = [1, 2, 3, 4, 5]; 122 | } 123 | 124 | } 125 | --- 126 | adds the previous block of the same type as the selection to each selection, ignoring token type 127 | --- 128 | envy:add-block-selection-backward-alternative 129 | --- 130 | /*<| |><|Comment|><| |>*/<| 131 | |><|class|><| |>A { 132 | 133 | b(c) { 134 | d = "abcdefg"; 135 | e = [1, 2, 3, 4, 5]; 136 | } 137 | 138 | } 139 | --- 140 | -------------------------------------------------------------------------------- /spec/tests/bracket-selection-1.test: -------------------------------------------------------------------------------- 1 | /* <||>Comment */ 2 | class A { 3 | b(c) { 4 | d = "<||>abcdefg"; 5 | e = [1, <|2, 3|>, 4, 5]; 6 | } 7 | } 8 | --- 9 | expands each selection to the nearest surrounding bracket pair 10 | --- 11 | envy:select-surrounding-brackets 12 | --- 13 | /*<| Comment |>*/ 14 | class A { 15 | b(c) { 16 | d = "<|abcdefg|>"; 17 | e = [<|1, 2, 3, 4, 5|>]; 18 | } 19 | } 20 | --- 21 | expands selections that completely cover the inside of a bracket pair to the outside of that pair 22 | --- 23 | envy:select-surrounding-brackets 24 | --- 25 | <|/* Comment */|> 26 | class A { 27 | b(c) { 28 | d = <|"abcdefg"|>; 29 | e = <|[1, 2, 3, 4, 5]|>; 30 | } 31 | } 32 | --- 33 | merges selections that expand to the same surrounding bracket pair 34 | --- 35 | envy:select-surrounding-brackets 36 | --- 37 | <|/* Comment */|> 38 | class A { 39 | b(c) {<| 40 | d = "abcdefg"; 41 | e = [1, 2, 3, 4, 5]; 42 | |>} 43 | } 44 | --- 45 | expands selections to nested bracket pairs 46 | --- 47 | envy:select-surrounding-brackets 48 | envy:select-surrounding-brackets 49 | envy:select-surrounding-brackets 50 | --- 51 | <|/* Comment */|> 52 | class A <|{ 53 | b(c) { 54 | d = "abcdefg"; 55 | e = [1, 2, 3, 4, 5]; 56 | } 57 | }|> 58 | --- 59 | does nothing when there are no surrounding matched brackets 60 | --- 61 | envy:select-surrounding-brackets 62 | --- 63 | <|/* Comment */|> 64 | class A <|{ 65 | b(c) { 66 | d = "abcdefg"; 67 | e = [1, 2, 3, 4, 5]; 68 | } 69 | }|> 70 | --- 71 | -------------------------------------------------------------------------------- /spec/tests/bracket-selection-2.test: -------------------------------------------------------------------------------- 1 | /* Comment */ 2 | class A { 3 | b<|(c)|> { 4 | d = "abcdefg"; 5 | e = [1, 2, 3, 4, 5]; 6 | } 7 | } 8 | --- 9 | selects all bracket pairs 10 | --- 11 | envy:select-all-brackets 12 | --- 13 | <|/* Comment */|> 14 | class A { 15 | b<|(c)|> <|{ 16 | d = "abcdefg"; 17 | e = [1, 2, 3, 4, 5]; 18 | }|> 19 | } 20 | --- 21 | -------------------------------------------------------------------------------- /spec/tests/bracket-selection-3.test: -------------------------------------------------------------------------------- 1 | /* Comment */ 2 | class A { 3 | b(c) { 4 | d = "<||>abcdefg"; 5 | e = [1, <|2, 3|>, 4, 5]; 6 | } 7 | } 8 | --- 9 | moves each selection to the previous bracket pair 10 | --- 11 | envy:move-bracket-selection-backward 12 | --- 13 | /* Comment */ 14 | class A { 15 | b<|(c)|> { 16 | d = <|"abcdefg"|>; 17 | e = [1, 2, 3, 4, 5]; 18 | } 19 | } 20 | --- 21 | moves each selection to the next bracket pair 22 | --- 23 | envy:move-bracket-selection-forward 24 | --- 25 | /* Comment */ 26 | class A { 27 | b(c) <|{ 28 | d = "abcdefg"; 29 | e = [1, 2, 3, 4, 5]; 30 | }|> 31 | } 32 | --- 33 | moves each selection to the previous bracket pair 34 | --- 35 | envy:move-bracket-selection-backward 36 | --- 37 | /* Comment */ 38 | class A { 39 | b<|(c)|> { 40 | d = "abcdefg"; 41 | e = [1, 2, 3, 4, 5]; 42 | } 43 | } 44 | --- 45 | adds the next bracket pair to each selection 46 | --- 47 | envy:add-bracket-selection-forward 48 | --- 49 | /* Comment */ 50 | class A { 51 | b<|(c)|> <|{ 52 | d = "abcdefg"; 53 | e = [1, 2, 3, 4, 5]; 54 | }|> 55 | } 56 | --- 57 | adds the previous bracket pair to each selection 58 | --- 59 | envy:add-bracket-selection-backward 60 | --- 61 | <|/* Comment */|> 62 | class A { 63 | b<|(c)|> <|{ 64 | d = "abcdefg"; 65 | e = [1, 2, 3, 4, 5]; 66 | }|> 67 | } 68 | --- 69 | -------------------------------------------------------------------------------- /spec/tests/invert-selections.test: -------------------------------------------------------------------------------- 1 | abcd efg hijkl mnop 2 | qrs tu<|vwx yz. 3 | 123 |>4567890 4 | <||> 5 | abcd efg <||>hijkl mnop 6 | <|qrs tuvwx yz. 7 | |>123 4567890 8 | --- 9 | inverts each selection inside the lines it contains 10 | --- 11 | envy:invert-selections-in-lines 12 | --- 13 | abcd efg hijkl mnop 14 | <|qrs tu|>vwx yz. 15 | 123 <|4567890|> 16 | <||> 17 | <|abcd efg |><|hijkl mnop|> 18 | <|qrs tuvwx yz. 19 | |>123 4567890 20 | --- 21 | (mostly) restores the original selections when inverting twice 22 | --- 23 | envy:invert-selections-in-lines 24 | --- 25 | abcd efg hijkl mnop 26 | qrs tu<|vwx yz.|> 27 | <|123 |>4567890 28 | <||> 29 | abcd efg <||>hijkl mnop 30 | <|qrs tuvwx yz. 31 | |>123 4567890 32 | --- 33 | inverts all selections 34 | --- 35 | envy:invert-selections 36 | --- 37 | <|abcd efg hijkl mnop 38 | qrs tu|>vwx yz.<| 39 | |>123 <|4567890 40 | |><| 41 | abcd efg |><|hijkl mnop 42 | |>qrs tuvwx yz. 43 | <|123 4567890|> 44 | --- 45 | restores the original selections when inverting twice 46 | --- 47 | envy:invert-selections 48 | --- 49 | abcd efg hijkl mnop 50 | qrs tu<|vwx yz.|> 51 | <|123 |>4567890 52 | <||> 53 | abcd efg <||>hijkl mnop 54 | <|qrs tuvwx yz. 55 | |>123 4567890 56 | --- 57 | -------------------------------------------------------------------------------- /spec/tests/match-selection-1.test: -------------------------------------------------------------------------------- 1 | <|a|>bc <|def|> abc abc 2 | ghi def ghi GHI jkl 3 | abcdefghijklmnopqrstuvwxyz<| 4 | abcdefghijklmnopqrstuvwxyz|> 5 | --- 6 | selects all occurrences of each selected text in the same line as the selection 7 | --- 8 | envy:select-all-matches-in-lines 9 | --- 10 | <|a|>bc <|def|> <|a|>bc <|a|>bc 11 | ghi def ghi GHI jkl 12 | abcdefghijklmnopqrstuvwxyz<| 13 | abcdefghijklmnopqrstuvwxyz|> 14 | --- 15 | selects all occurrences of each selected text 16 | --- 17 | envy:select-all-matches 18 | --- 19 | <|a|>bc <|def|> <|a|>bc <|a|>bc 20 | ghi <|def|> ghi GHI jkl<| 21 | abcdefghijklmnopqrstuvwxyz|><| 22 | abcdefghijklmnopqrstuvwxyz|> 23 | --- 24 | -------------------------------------------------------------------------------- /spec/tests/match-selection-2.test: -------------------------------------------------------------------------------- 1 | <|abc |><|def|> abc abc 2 | ghi def <|ghi |>GHI jkl<| 3 | a|>bcdefghijklmnopqrstuvwxyz 4 | abcdefghijklmnopqrstuvwxyz 5 | --- 6 | moves each selection to the next occurrence of the selected text 7 | --- 8 | envy:select-next-match 9 | --- 10 | abc def <|abc |>abc 11 | ghi <|def|> <|ghi |>GHI jkl 12 | abcdefghijklmnopqrstuvwxyz<| 13 | a|>bcdefghijklmnopqrstuvwxyz 14 | --- 15 | moves each selection to the previous occurrence of the selected text 16 | --- 17 | envy:select-previous-match 18 | --- 19 | <|abc |><|def|> abc abc 20 | <|ghi |>def ghi GHI jkl<| 21 | a|>bcdefghijklmnopqrstuvwxyz 22 | abcdefghijklmnopqrstuvwxyz 23 | --- 24 | adds the next occurrence of the selected text to each selection 25 | --- 26 | envy:add-select-next-match 27 | --- 28 | <|abc |><|def|> <|abc |>abc 29 | <|ghi |><|def|> <|ghi |>GHI jkl<| 30 | a|>bcdefghijklmnopqrstuvwxyz<| 31 | a|>bcdefghijklmnopqrstuvwxyz 32 | --- 33 | merges selections with identical text when moving them to the same position 34 | --- 35 | envy:select-next-match 36 | --- 37 | abc def <|abc |>abc 38 | ghi <|def|> <|ghi |>GHI jkl 39 | abc<|def|>ghijklmnopqrstuvwxyz<| 40 | a|>bcdefghijklmnopqrstuvwxyz 41 | --- 42 | adds the previous occurrence of the selected text to each selection 43 | --- 44 | envy:add-select-previous-match 45 | --- 46 | <|abc |><|def|> <|abc |>abc 47 | <|ghi |><|def|> <|ghi |>GHI jkl<| 48 | a|>bc<|def|>ghijklmnopqrstuvwxyz<| 49 | a|>bcdefghijklmnopqrstuvwxyz 50 | --- 51 | -------------------------------------------------------------------------------- /spec/tests/remove-selections.test: -------------------------------------------------------------------------------- 1 | <|abcd|> <|efg|> hi<|jkl mnop 2 | qr|><|s|> tuvwx<||> <|yz. 3 | 123 456|>7890 4 | --- 5 | removes all even-indexed selections 6 | --- 7 | envy:remove-every-second-selection-even 8 | --- 9 | <|abcd|> efg hi<|jkl mnop 10 | qr|>s tuvwx<||> yz. 11 | 123 4567890 12 | --- 13 | removes all odd-indexed selections 14 | --- 15 | envy:remove-every-second-selection-odd 16 | --- 17 | abcd efg hi<|jkl mnop 18 | qr|>s tuvwx yz. 19 | 123 4567890 20 | --- 21 | -------------------------------------------------------------------------------- /spec/tests/rotate-selections.test: -------------------------------------------------------------------------------- 1 | <|abcd|> <|efg|> hi<|jkl mnop 2 | qr|> <|s|> tuvwx<||> <|yz. 3 | 123 456|>7890 4 | --- 5 | cyclically replaces the text of each selection with that of the preceding one 6 | --- 7 | envy:rotate-selections-forward 8 | --- 9 | <|yz. 10 | 123 456|> <|abcd|> hi<|efg|> <|jkl mnop 11 | qr|> tuvwx<|s|> <||>7890 12 | --- 13 | restores the original text when repeating the process often enough 14 | --- 15 | envy:rotate-selections-forward 16 | envy:rotate-selections-forward 17 | envy:rotate-selections-forward 18 | envy:rotate-selections-forward 19 | envy:rotate-selections-forward 20 | --- 21 | <|abcd|> <|efg|> hi<|jkl mnop 22 | qr|> <|s|> tuvwx<||> <|yz. 23 | 123 456|>7890 24 | --- 25 | cyclically replaces the text of each selection with that of the following one 26 | --- 27 | envy:rotate-selections-backward 28 | --- 29 | <|efg|> <|jkl mnop 30 | qr|> hi<|s|> <||> tuvwx<|yz. 31 | 123 456|> <|abcd|>7890 32 | --- 33 | restores the original text when repeating the process often enough 34 | --- 35 | envy:rotate-selections-backward 36 | envy:rotate-selections-backward 37 | envy:rotate-selections-backward 38 | envy:rotate-selections-backward 39 | envy:rotate-selections-backward 40 | --- 41 | <|abcd|> <|efg|> hi<|jkl mnop 42 | qr|> <|s|> tuvwx<||> <|yz. 43 | 123 456|>7890 44 | --- 45 | -------------------------------------------------------------------------------- /spec/tests/secondary-clipboard.test: -------------------------------------------------------------------------------- 1 | <|abcd|> efg hijkl mnop 2 | qrs tuvwx yz. 3 | 123 4567890 4 | --- 5 | copies and pastes a single selection 6 | --- 7 | envy:copy-to-secondary-clipboard 8 | core:move-right 9 | envy:paste-from-secondary-clipboard 10 | --- 11 | abcdabcd<||> efg hijkl mnop 12 | qrs tuvwx yz. 13 | 123 4567890 14 | --- 15 | cuts and pastes a single selection 16 | --- 17 | editor:select-to-beginning-of-line 18 | envy:cut-to-secondary-clipboard 19 | editor:move-to-end-of-line 20 | envy:paste-from-secondary-clipboard 21 | --- 22 | efg hijkl mnopabcdabcd<||> 23 | qrs tuvwx yz. 24 | 123 4567890 25 | --- 26 | copies and pastes the entire line if the selection is empty 27 | --- 28 | envy:copy-to-secondary-clipboard 29 | core:move-right 30 | envy:paste-from-secondary-clipboard 31 | --- 32 | efg hijkl mnopabcdabcd 33 | efg hijkl mnopabcdabcd 34 | <||>qrs tuvwx yz. 35 | 123 4567890 36 | --- 37 | cuts and pastes the entire line if the selection is empty 38 | --- 39 | envy:cut-to-secondary-clipboard 40 | core:move-up 41 | envy:paste-from-secondary-clipboard 42 | --- 43 | efg hijkl mnopabcdabcd 44 | qrs tuvwx yz. 45 | <||> efg hijkl mnopabcdabcd 46 | 123 4567890 47 | --- 48 | prepares the selections for the next test 49 | --- 50 | envy:move-block-selection-forward-alternative 51 | envy:add-block-selection-forward 52 | envy:add-block-selection-forward 53 | --- 54 | efg hijkl mnopabcdabcd 55 | qrs tuvwx yz. 56 | <|efg|> <|hijkl|> <|mnopabcdabcd|> 57 | 123 4567890 58 | --- 59 | pastes the corresponding clipboard item if the number of cursors equals the number of clipboard items 60 | --- 61 | envy:copy-to-secondary-clipboard 62 | core:move-right 63 | envy:paste-from-secondary-clipboard 64 | --- 65 | efg hijkl mnopabcdabcd 66 | qrs tuvwx yz. 67 | efgefg<||> hijklhijkl<||> mnopabcdabcdmnopabcdabcd<||> 68 | 123 4567890 69 | --- 70 | pastes each clipboard item on a separate line if the number of cursors does not equal the number of clipboard items 71 | --- 72 | editor:move-to-end-of-line 73 | envy:paste-from-secondary-clipboard 74 | --- 75 | efg hijkl mnopabcdabcd 76 | qrs tuvwx yz. 77 | efgefg hijklhijkl mnopabcdabcdmnopabcdabcdefg 78 | hijkl 79 | mnopabcdabcd<||> 80 | 123 4567890 81 | --- 82 | -------------------------------------------------------------------------------- /spec/tests/split-selections.test: -------------------------------------------------------------------------------- 1 | <|abcd|> <|efg|> hi<|jkl mnop 2 | qr|><|s|> tuvwx<||> <|yz. 3 | 123 456|>7890 4 | --- 5 | splits each selection into two cursors 6 | --- 7 | envy:split-selections-into-cursors 8 | --- 9 | <||>abcd<||> <||>efg<||> hi<||>jkl mnop 10 | qr<||>s<||> tuvwx<||> <||>yz. 11 | 123 456<||>7890 12 | --- 13 | -------------------------------------------------------------------------------- /spec/tests/swap-selections.test: -------------------------------------------------------------------------------- 1 | <|abcd|> <|efg|> hi<|jkl mnop 2 | qr|><|s|> tuvwx<||> <|yz. 3 | 123 456|>789<|0|> 4 | --- 5 | swaps the texts of each pair of selections (1 <-> 2, 3 <-> 4 etc.) 6 | --- 7 | envy:swap-selections 8 | --- 9 | <|efg|> <|abcd|> hi<|s|><|jkl mnop 10 | qr|> tuvwx<|yz. 11 | 123 456|> <||>789<|0|> 12 | --- 13 | restores the original text when swapping twice 14 | --- 15 | envy:swap-selections 16 | --- 17 | <|abcd|> <|efg|> hi<|jkl mnop 18 | qr|><|s|> tuvwx<||> <|yz. 19 | 123 456|>789<|0|> 20 | --- 21 | swaps the texts of each pair of selections (1 <-> 3, 2 <-> 4 etc.) 22 | --- 23 | envy:swap-selections-alternative 24 | --- 25 | <|jkl mnop 26 | qr|> <|s|> hi<|abcd|><|efg|> tuvwx<|0|> <|yz. 27 | 123 456|>789<||> 28 | --- 29 | restores the original text when swapping twice 30 | --- 31 | envy:swap-selections-alternative 32 | --- 33 | <|abcd|> <|efg|> hi<|jkl mnop 34 | qr|><|s|> tuvwx<||> <|yz. 35 | 123 456|>789<|0|> 36 | --- 37 | -------------------------------------------------------------------------------- /styles/envy.less: -------------------------------------------------------------------------------- 1 | /* 2 | * Envy - Text editing supercharger 3 | * 4 | * Copyright (c) 2017 Philipp Emanuel Weidmann 5 | * 6 | * Nemo vir est qui mundum non reddat meliorem. 7 | * 8 | * Released under the terms of the MIT License 9 | * (https://opensource.org/licenses/MIT) 10 | */ 11 | 12 | @import 'syntax-variables'; 13 | 14 | @envy-background: darken(@syntax-background-color, 5%); 15 | @envy-cursor: contrast(@envy-background, orange, yellow); 16 | 17 | atom-text-editor.envy-mode { 18 | background-color: @envy-background !important; 19 | 20 | .selection .region { 21 | background-color: @envy-cursor !important; 22 | } 23 | 24 | .cursor { 25 | border-left: 3px solid @envy-cursor !important; 26 | } 27 | 28 | .cursors.blink-off .cursor { 29 | opacity: 1 !important; 30 | } 31 | 32 | .line.cursor-line { 33 | background-color: transparent !important; 34 | } 35 | } 36 | --------------------------------------------------------------------------------