├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── atomic-emacs.cson ├── lib ├── atomic-emacs.coffee ├── completer.coffee ├── emacs-cursor.coffee ├── emacs-editor.coffee ├── kill-ring.coffee ├── mark.coffee ├── search-manager.coffee ├── search-results.coffee ├── search-view.coffee ├── search.coffee ├── state.coffee └── utils.coffee ├── package.json ├── spec ├── atomic-emacs-spec.coffee ├── emacs-cursor-spec.coffee ├── emacs-editor-spec.coffee ├── kill-ring-spec.coffee ├── mark-spec.coffee ├── search-results-spec.coffee ├── search-spec.coffee ├── test-editor-spec.coffee ├── test-editor.coffee └── utils-spec.coffee └── styles └── atomic-emacs.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.15.0 (2019-08-19) 2 | 3 | * Bind tab to editor:auto-indent. 4 | * Document how to extend Atomic Emacs in README. 5 | * Fix error when using find-and-replace:select-next from isearch input. 6 | 7 | ## 0.14.0 (2019-05-12) 8 | 9 | * Incremental search (ctrl-s, ctrl-r), see README for more information. 10 | 11 | ## 0.13.0 (2019-01-07) 12 | 13 | * Add dabbrev-{expand,previous} (alt-/, alt-?). 14 | * alt-. now binds to symbols-view:go-to-declaration. 15 | * Expose AtomicEmacs object as a service, for writing extensions. 16 | * Fix kill ring entry when all cursors are merged. 17 | * Bind editor:auto-indent to ctrl-alt-\. 18 | 19 | ## 0.12.1 (2017-09-06) 20 | 21 | * Fix finding a file when no tabs are open. 22 | * Don't close docks when calling close-other-panes. 23 | 24 | ## 0.12.0 (2017-08-15) 25 | 26 | * Add delete-blank-lines (ctrl-x ctrl-o). 27 | * Add {forward,backward}-list (ctrl-alt-n, ctrl-alt-p). [Philip Larie] 28 | * Bind editor:newline to ctrl-m. 29 | * Use advanced-open-file if installed for ctrl-x ctrl-f (can be disabled in 30 | settings). 31 | * Fix killing & yanking with multiple cursors. 32 | * Fix open-line moving the cursor if autoindentation is on. 33 | * Fix editing commands in settings pane under Atom 1.19 [Adrien Delessert]. 34 | 35 | ## 0.11.0 (2017-05-15) 36 | 37 | * Add option to have clipboard copies appended to kill ring. [Marty Gentillon] 38 | * Killing with multiple cursors also appends to global kill ring. [Marty 39 | Gentillon] 40 | 41 | ## 0.10.0 (2017-02-27) 42 | 43 | * Add transpose-sexps (ctrl-alt-t). 44 | * Setting to make built-in cut & copy commands use the kill ring. 45 | * Fix error when closing a tab with the mark active. 46 | * Fix some bindings being shadowed by Atom Core on Linux & Windows. 47 | * Fix commands potentially firing twice after upgrading Atomic Emacs. 48 | * Address deprecation warnings in recent Atom versions. 49 | 50 | ## 0.9.2 (2016-06-18) 51 | 52 | * Fix scroll-{up,down} in an empty editor. 53 | 54 | ## 0.9.1 (2016-03-26) 55 | 56 | * New bindings: 57 | * ctrl-x ctrl-c: application:quit [Josh Meyer] 58 | * ctrl-x u: core:undo [Josh Meyer] 59 | * copy-region-as-kill writes to clipboard, like other kill commands. [Yuichi 60 | Tanikawa] 61 | * Fix selection disappearing when moving past the ends of the buffer. 62 | * Added note to readme about key binding collisions on Windows. 63 | 64 | ## 0.9.0 (2016-01-02) 65 | 66 | * Updated readme. 67 | * Killing and yanking commands, multi-cursor aware. See readme for details. 68 | * transpose-{chars,words,lines} now works with multiple cursors. 69 | * Add "alt-g alt-g" as an alias for go-to-line:toggle. 70 | * Fix delete-indentation. 71 | * Fix issue with selection jumping erratically when moving after a mark-sexp. 72 | * open-line no longer jumps to the start of the line. 73 | * Fix undo behavior of just-one-space, {{up,down}case,capitalize}-word-or-region. 74 | 75 | ## 0.8.0 (2015-12-03) 76 | * C-v, M-v now consistently moves half a screen up/down. 77 | * C-l now cycles through middle-top-bottom, like Emacs' default. 78 | 79 | ## 0.7.4 (2015-10-05) 80 | * C-g now cancels auto-complete and multiple cursors, like escape. 81 | 82 | ## 0.7.3 (2015-09-05) 83 | * Fix crash on latest Atom when clearing the region. 84 | 85 | ## 0.7.2 (2015-07-19) 86 | * Remove deprecated .editor selector 87 | * Fix paragraph movement commands 88 | * Fix upcasing & downcasing, add capitalizing, work on words or selections. 89 | * C-x 1 now closes other panes, not other tabs. 90 | * Add start/end of line key bindings for Windows 91 | * Don't override ctrl-a on win32. 92 | * Bind "ctrl-k ctrl-k" to cut-to-end-of-line on win32. 93 | 94 | ## 0.7.1 (2015-06-20) 95 | * Remove keybindings that don't make sense in mini-editors 96 | * Replace autocomplete keybindings with autocomplete-plus 97 | * Make alt-{left,right} the same as alt-{b,f}. 98 | 99 | ## 0.7.0 (2015-06-10) 100 | * Fix behavior of ctrl-a and alt-m 101 | * Fix behavior of keybindings when autocomplete menu is active 102 | * Make commands work in mini editors 103 | * Fix deprecation in delete-indentation 104 | * Fix transpose-chars 105 | * Add missing activation commands 106 | 107 | ## 0.6.0 (2015-06-01) 108 | * Remove usage of deprecated APIs 109 | * Add delete indentation 110 | 111 | ## 0.5.1 (2015-04-28) 112 | * Fix remove mark 113 | 114 | ## 0.5.0 (2015-04-08) 115 | * Move out of using deprecated APIs 116 | 117 | ## 0.4.2 (2015-04-02) 118 | * Do not override keymappings when autocomplete is active 119 | 120 | ## 0.4.1 (2015-03-16) 121 | * Add ctrl-a and ctrl-e keybindings for Linux 122 | 123 | ## 0.4.0 (2015-01-27) 124 | * Added a setting to use core navigation keys instead of the atomic-emacs versions 125 | 126 | ## 0.3.7 (2015-01-20) 127 | * Bind `next-paragraph` and `previous-paragraph` to `M-}` and `M-{` respectively. 128 | 129 | ## 0.3.6 (2015-01-03) 130 | * Fix `next-line` and `previous-line` skipping bug. 131 | 132 | ## 0.3.5 (2014-11-30) 133 | * Bind ctrl-k to `editor:cut-to-end-of-line` 134 | 135 | ## 0.3.4 (2014-11-23) 136 | * Rename `autoflow:reflow-paragraph` to `autoflow:reflow-selection` because of upstream change. 137 | * Bind ctrl-k to `editor:delete-to-end-of-line` 138 | 139 | ## 0.3.3 (2014-09-24) 140 | * Fixed for atom update 0.130.0. 141 | 142 | ## 0.3.2 (2014-07-27) 143 | * Fixed the recenter command. 144 | * Fixed the test suite. 145 | * Enabled travis ci. 146 | 147 | ## 0.3.1 (2014-07-13) 148 | * Fixed a bug where the editor is not being accessible inside transact. 149 | 150 | ## 0.3.0 (2014-07-12) 151 | * Make atomic-emacs work with React. 152 | 153 | ## 0.2.13 (2014-06-18) 154 | * Partial fix for Uncaught TypeError, issue #17 155 | 156 | ## 0.2.12 (2014-05-27) 157 | * previous-line and next-line now works for the command palette. 158 | * tab now works as expected when the mark is active. 159 | 160 | ## 0.2.11 (2014-04-11) 161 | * Because of a tagging mishap, what should have been 0.2.9 became 0.2.11 162 | * Tags that weren't deleted were deleted, but I forgot to update package.json. Hence 0.2.11. Bummer. 163 | 164 | ## 0.2.9 (2014-04-11) 165 | * Arrow keys should now work properly with set-mark. 166 | * ctrl-n and ctrl-p should now work as expected in fuzzy-finder. 167 | * alt-w now uses the new mark deactivate API. 168 | 169 | ## 0.2.8 (2014-04-08) 170 | * M-q bound to reflow-paragraph. 171 | * Movement by words are now more emacs-like. 172 | 173 | ## 0.2.7 (2014-04-01) 174 | * Mark improvements. 175 | 176 | ## 0.2.6 (2014-03-20) 177 | * Added movement by paragraph with marks support. Not yet 100% emacs compatible. 178 | 179 | ## 0.2.5 (2014-03-18) 180 | * Added alt-t as transpose-words. 181 | * Improved transpose-lines. Indents are now included in the transposed lines. 182 | * New API for cursors. 183 | 184 | ## 0.2.4 (2014-03-13) 185 | * Set mark will now retain selection when moving by words. 186 | * Added the recenter-top-bottom command. 187 | * Added the just-one-space command. 188 | * Added the delete-horizontal-space command. 189 | 190 | ## 0.2.3 (2014-03-08) 191 | * ctrl-v and alt-v will now retain the mark if active. 192 | * changed binding of alt-/ from `autocomplete:attach` to `autocomplete:toggle`. 193 | * Bind ctrl-s and ctrl-r to find next/prev when in `.find-and-replace`. 194 | 195 | ## 0.2.2 (2014-03-08) 196 | * Fixed a bug where the next and previous line commands cause a crash when used in the file finder without an editor opened. 197 | * Moved some keybindings to `.body` so that they can be used in other views, ie. settings view. 198 | 199 | ## 0.2.1 (2014-03-04) 200 | 201 | * Fixed a bug where the selection can only move in the y-axis. 202 | * Improved set-mark. It will now retain the selection on most of the current motion commands. Still have to figure out how to make this work with ctrl-v and alt-v. 203 | * ctrl-g will now cancel the selection. 204 | 205 | ## 0.2.0 (2014-03-01) 206 | 207 | * Added marks to select an arbitrary group of text. This should be mapped by the user because the core ctrl-space mapping can't be overriden by this package. 208 | * Added transpose characters. 209 | * Added an emacs style copy mapped to alt-w. 210 | * Added alt-; mapping for toggling line comments. 211 | * The mark's head and tail can be swapped with ctrl-x ctrl-x. 212 | * Transposing lines now make use of the new transactions API. 213 | 214 | ## 0.1.1 (2014-02-28) 215 | 216 | * Rebound some more keys. 217 | * Ctrl-g will now attempt to cancel an action. Works most of the time except for the editor, because the core Go To Line keymap which can't be overridden it seems. 218 | 219 | ## 0.1.0 (2014-02-27) 220 | 221 | * Initial Release. Lots of things missing. 222 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 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 | ## Atomic Emacs 2 | 3 | Emacs keybindings for Atom. 4 | ![Build Status](https://travis-ci.org/avendael/atomic-emacs.svg?branch=master) 5 | 6 | ## Installation 7 | 8 | On the command line: 9 | 10 | * `apm install atomic-emacs` 11 | 12 | Or in Atom: 13 | 14 | * In `Preferences`, click the `Install` tab. 15 | * Type `atomic-emacs` in the search box, and click the `Packages` button. 16 | * Click `Install` on the `atomic-emacs` package. 17 | 18 | There's no need to restart Atom. 19 | 20 | ## Commands 21 | 22 | ### Navigation 23 | 24 | 'ctrl-b': 'atomic-emacs:backward-char' 25 | 'left': 'atomic-emacs:backward-char' 26 | 'ctrl-f': 'atomic-emacs:forward-char' 27 | 'right': 'atomic-emacs:forward-char' 28 | 'alt-b': 'atomic-emacs:backward-word' 29 | 'alt-left': 'atomic-emacs:backward-word' 30 | 'alt-f': 'atomic-emacs:forward-word' 31 | 'alt-right': 'atomic-emacs:forward-word' 32 | 'ctrl-alt-b': 'atomic-emacs:backward-sexp' 33 | 'ctrl-alt-f': 'atomic-emacs:forward-sexp' 34 | 'ctrl-alt-p': 'atomic-emacs:backward-list' 35 | 'ctrl-alt-n': 'atomic-emacs:forward-list' 36 | 'alt-{': 'atomic-emacs:backward-paragraph' 37 | 'alt-}': 'atomic-emacs:forward-paragraph' 38 | 'alt-m': 'atomic-emacs:back-to-indentation' 39 | 'ctrl-a': 'editor:move-to-beginning-of-line' 40 | 'alt-<': 'core:move-to-top' 41 | 'alt->': 'core:move-to-bottom' 42 | 43 | ### Killing & Yanking 44 | 45 | 'alt-backspace': 'atomic-emacs:backward-kill-word' 46 | 'alt-delete': 'atomic-emacs:backward-kill-word' 47 | 'alt-d': 'atomic-emacs:kill-word' 48 | 'ctrl-k': 'atomic-emacs:kill-line' 49 | 'ctrl-w': 'atomic-emacs:kill-region' 50 | 'alt-w': 'atomic-emacs:copy-region-as-kill' 51 | 'ctrl-alt-w': 'atomic-emacs:append-next-kill' 52 | 'ctrl-y': 'atomic-emacs:yank' 53 | 'alt-y': 'atomic-emacs:yank-pop' 54 | 'alt-shift-y': 'atomic-emacs:yank-shift' 55 | 56 | Note that Atomic Emacs does not (yet) support prefix arguments, so to rotate the 57 | kill ring forward, use `yank-shift` (equivalent to `yank-pop` in Emacs with a 58 | prefix argument of -1). 59 | 60 | ### Editing 61 | 62 | 'alt-\\': 'atomic-emacs:delete-horizontal-space' 63 | 'alt-^': 'atomic-emacs:delete-indentation' 64 | 'ctrl-o': 'atomic-emacs:open-line' 65 | 'alt-space': 'atomic-emacs:just-one-space' 66 | 'ctrl-x ctrl-o': 'atomic-emacs:delete-blank-lines' 67 | 'ctrl-t': 'atomic-emacs:transpose-chars' 68 | 'alt-t': 'atomic-emacs:transpose-words' 69 | 'ctrl-alt-t': 'atomic-emacs:transpose-sexps' 70 | 'ctrl-x ctrl-t': 'atomic-emacs:transpose-lines' 71 | 'ctrl-x ctrl-l': 'atomic-emacs:downcase-word-or-region' 72 | 'alt-l': 'atomic-emacs:downcase-word-or-region' 73 | 'ctrl-x ctrl-u': 'atomic-emacs:upcase-word-or-region' 74 | 'alt-u': 'atomic-emacs:upcase-word-or-region' 75 | 'alt-c': 'atomic-emacs:capitalize-word-or-region' 76 | 'ctrl-j': 'editor:newline' 77 | 'ctrl-m': 'editor:newline' 78 | 'ctrl-/': 'core:undo' 79 | 'ctrl-_': 'core:undo' 80 | 'ctrl-x u': 'core:undo' 81 | 'alt-/': 'atomic-emacs:dabbrev-expand' 82 | 'alt-?': 'atomic-emacs:dabbrev-previous' 83 | 'alt-q': 'autoflow:reflow-selection' 84 | 'alt-;': 'editor:toggle-line-comments' 85 | 'ctrl-alt-\\' : 'editor:auto-indent' 86 | 87 | ### Searching 88 | 89 | 'ctrl-s': 'atomic-emacs:isearch-forward' 90 | 'ctrl-r': 'atomic-emacs:isearch-backward' 91 | 92 | While searching: 93 | 94 | 'enter': 'atomic-emacs:isearch-exit' 95 | 'ctrl-m': 'atomic-emacs:isearch-exit' 96 | 'escape': 'atomic-emacs:isearch-cancel' 97 | 'ctrl-g': 'atomic-emacs:isearch-cancel' 98 | 'ctrl-s': 'atomic-emacs:isearch-repeat-forward' 99 | 'ctrl-r': 'atomic-emacs:isearch-repeat-backward' 100 | 'alt-s c': 'atomic-emacs:isearch-toggle-case-fold' 101 | 'alt-c': 'atomic-emacs:isearch-toggle-case-fold' 102 | 'alt-s r': 'atomic-emacs:isearch-toggle-regexp' 103 | 'alt-r': 'atomic-emacs:isearch-toggle-regexp' 104 | 'ctrl-w': 'atomic-emacs:isearch-yank-word-or-character' 105 | 106 | ### Marking & Selecting 107 | 108 | 'ctrl-space': 'atomic-emacs:set-mark' 109 | 'ctrl-alt-space': 'atomic-emacs:mark-sexp' 110 | 'ctrl-x h': 'atomic-emacs:mark-whole-buffer' 111 | 'ctrl-x ctrl-x': 'atomic-emacs:exchange-point-and-mark' 112 | 113 | ### UI 114 | 115 | 'ctrl-g': 'core:cancel' 116 | 'ctrl-x ctrl-s': 'core:save' 117 | 'ctrl-x ctrl-w': 'core:save-as' 118 | 'alt-x': 'command-palette:toggle' 119 | 'alt-.': 'symbols-view:go-to-declaration' 120 | 'ctrl-x ctrl-c': 'application:quit' 121 | 'ctrl-x ctrl-f': 'atomic-emacs:find-file' 122 | 'ctrl-x b': 'fuzzy-finder:toggle-buffer-finder' 123 | 'ctrl-x k': 'core:close' 124 | 'ctrl-x 0': 'pane:close' 125 | 'ctrl-x 1': 'atomic-emacs:close-other-panes' 126 | 'ctrl-x 2': 'pane:split-down' 127 | 'ctrl-x 3': 'pane:split-right' 128 | 'ctrl-x o': 'window:focus-next-pane' 129 | 130 | ### Other Packages 131 | 132 | For a more Emacs-like version of `find-file`, install 133 | [`advanced-open-file`](https://atom.io/packages/advanced-open-file). Atomic 134 | Emacs will use that package if it exists by default instead of Atom's 135 | fuzzy-finder. This may be disabled in settings, but note that fuzzy-finder 136 | cannot create new files. 137 | 138 | ### Extending Atomic Emacs 139 | 140 | Atomic Emacs exposes its core classes via a consumable service. For example, 141 | here's how you could extend it to add subword navigation: 142 | 143 | `~/.atom/init.coffee`: 144 | ```coffeescript 145 | atom.workspace.packageManager.serviceHub.consume "atomic-emacs", "^0.13.0", (service) -> 146 | window.emacs = service 147 | 148 | forwardSubword = (event) -> 149 | emacsEditor = emacs.getEditor(event) 150 | emacsEditor.moveEmacsCursors (emacsCursor) -> 151 | emacsCursor.skipNonWordCharactersForward() 152 | emacsCursor.cursor.moveToNextSubwordBoundary() 153 | 154 | backwardSubword = (event) -> 155 | emacsEditor = emacs.getEditor(event) 156 | emacsEditor.moveEmacsCursors (emacsCursor) -> 157 | emacsCursor.skipNonWordCharactersBackward() 158 | emacsCursor.cursor.moveToPreviousSubwordBoundary() 159 | 160 | atom.commands.add 'atom-text-editor', 'me:forward-subword', (event) -> forwardSubword(event) 161 | atom.commands.add 'atom-text-editor', 'me:backward-subword', (event) -> backwardSubword(event) 162 | ``` 163 | 164 | `~/.atom/keymap.cson`: 165 | ```coffeescript 166 | 'atom-text-editor': 167 | 'ctrl-left': 'me:backward-subword' 168 | 'ctrl-right': 'me:forward-subword' 169 | ``` 170 | 171 | If you're writing an extension package, consume the service as described in the 172 | [Atom Flight Manual][atom-flight-manual]. 173 | 174 | [atom-flight-manual]: https://flight-manual.atom.io/behind-atom/sections/interacting-with-other-packages-via-services/ 175 | 176 | Documentation for the Atomic Emacs core classes is sparse, but a common starting 177 | point is to get the EmacsEditor for the event as above. The EmacsEditor and 178 | EmacsCursor classes in the Atomic Emacs source should be fairly easy to follow. 179 | 180 | ### Something missing? 181 | 182 | Feel free to suggest features on the Github issue tracker, or better yet, send a 183 | pull request! 184 | 185 | ## Windows Note 186 | 187 | Some common Emacs keystrokes conflict with the default key bindings on Atom for 188 | Windows in unexpected ways. For example, `ctrl-k` (kill-line on emacs) is a 189 | prefix key for a set of pane management commands in Atom for Windows. The result 190 | is that after pressing `ctrl-k`, Atom will wait for 2 seconds to determine if it 191 | should treat this as a full command, or the beginning of another command, making 192 | `kill-line` feel "slow". 193 | 194 | You can of course disable this by disabling the all built-in key bindings that 195 | start with `ctrl-k` in your `keymaps.config` file. You can also do this a little 196 | easier with the [disable-keybindings][disable-keybindings] package. 197 | 198 | [disable-keybindings]: https://atom.io/packages/disable-keybindings 199 | 200 | ## Contributing 201 | 202 | * [Bug reports](https://github.com/avendael/atomic-emacs/issues) 203 | * [Source](https://github.com/avendael/atomic-emacs) 204 | * Patches: Fork on Github, send pull request. 205 | * Include tests where practical. 206 | * Leave the version alone, or bump it in a separate commit. 207 | -------------------------------------------------------------------------------- /keymaps/atomic-emacs.cson: -------------------------------------------------------------------------------- 1 | 'atom-workspace': 2 | 'ctrl-g': 'core:cancel' 3 | 'alt-x': 'command-palette:toggle' 4 | 'ctrl-x ctrl-c': 'application:quit' 5 | 'ctrl-x k': 'core:close' 6 | 'ctrl-x ctrl-f': 'atomic-emacs:find-file' 7 | 'ctrl-x b': 'fuzzy-finder:toggle-buffer-finder' 8 | 'ctrl-x 3': 'pane:split-right' 9 | 'ctrl-x 2': 'pane:split-down' 10 | 'ctrl-x 0': 'pane:close' 11 | 'ctrl-x 1': 'atomic-emacs:close-other-panes' 12 | 'ctrl-x o': 'window:focus-next-pane' 13 | 14 | 'body.platform-darwin': 15 | 'cmd-x': 'atomic-emacs:cut' 16 | 'cmd-c': 'atomic-emacs:copy' 17 | 18 | 'body.platform-linux': 19 | 'shift-delete': 'atomic-emacs:cut' 20 | 'ctrl-insert': 'atomic-emacs:copy' 21 | 'ctrl-x': 'atomic-emacs:cut' 22 | 'ctrl-c': 'atomic-emacs:copy' 23 | 24 | # For command palette, buffer switcher, etc. on Linux. 25 | 'body.platform-linux .select-list atom-text-editor.mini': 26 | 'ctrl-p': 'core:move-up' 27 | 'ctrl-n': 'core:move-down' 28 | 29 | 'body.platform-win32': 30 | 'shift-delete': 'atomic-emacs:cut' 31 | 'ctrl-insert': 'atomic-emacs:copy' 32 | 'ctrl-x': 'atomic-emacs:cut' 33 | 'ctrl-c': 'atomic-emacs:copy' 34 | 35 | 'atom-workspace atom-text-editor': 36 | # Navigation 37 | 'ctrl-b': 'atomic-emacs:backward-char' 38 | 'left': 'atomic-emacs:backward-char' 39 | 'ctrl-f': 'atomic-emacs:forward-char' 40 | 'right': 'atomic-emacs:forward-char' 41 | 'alt-b': 'atomic-emacs:backward-word' 42 | 'alt-left': 'atomic-emacs:backward-word' 43 | 'alt-f': 'atomic-emacs:forward-word' 44 | 'alt-right': 'atomic-emacs:forward-word' 45 | 'ctrl-alt-b': 'atomic-emacs:backward-sexp' 46 | 'ctrl-alt-f': 'atomic-emacs:forward-sexp' 47 | 'ctrl-alt-p': 'atomic-emacs:backward-list' 48 | 'ctrl-alt-n': 'atomic-emacs:forward-list' 49 | 'alt-{': 'atomic-emacs:backward-paragraph' 50 | 'alt-}': 'atomic-emacs:forward-paragraph' 51 | 'alt-m': 'atomic-emacs:back-to-indentation' 52 | 'ctrl-a': 'editor:move-to-beginning-of-line' 53 | 'alt-<': 'core:move-to-top' 54 | 'alt->': 'core:move-to-bottom' 55 | 56 | # Killing & Yanking 57 | 'alt-backspace': 'atomic-emacs:backward-kill-word' 58 | 'alt-delete': 'atomic-emacs:backward-kill-word' 59 | 'alt-d': 'atomic-emacs:kill-word' 60 | 'ctrl-k': 'atomic-emacs:kill-line' 61 | 'ctrl-w': 'atomic-emacs:kill-region' 62 | 'alt-w': 'atomic-emacs:copy-region-as-kill' 63 | 'ctrl-alt-w': 'atomic-emacs:append-next-kill' 64 | 'ctrl-y': 'atomic-emacs:yank' 65 | 'alt-y': 'atomic-emacs:yank-pop' 66 | 'alt-shift-y': 'atomic-emacs:yank-shift' 67 | 68 | # Editing 69 | 'alt-\\': 'atomic-emacs:delete-horizontal-space' 70 | 'alt-^': 'atomic-emacs:delete-indentation' 71 | 'ctrl-o': 'atomic-emacs:open-line' 72 | 'alt-space': 'atomic-emacs:just-one-space' 73 | 'ctrl-x ctrl-o': 'atomic-emacs:delete-blank-lines' 74 | 'ctrl-t': 'atomic-emacs:transpose-chars' 75 | 'alt-t': 'atomic-emacs:transpose-words' 76 | 'ctrl-alt-t': 'atomic-emacs:transpose-sexps' 77 | 'ctrl-x ctrl-t': 'atomic-emacs:transpose-lines' 78 | 'ctrl-x ctrl-l': 'atomic-emacs:downcase-word-or-region' 79 | 'alt-l': 'atomic-emacs:downcase-word-or-region' 80 | 'ctrl-x ctrl-u': 'atomic-emacs:upcase-word-or-region' 81 | 'alt-u': 'atomic-emacs:upcase-word-or-region' 82 | 'alt-c': 'atomic-emacs:capitalize-word-or-region' 83 | 'ctrl-j': 'editor:newline' 84 | 'ctrl-m': 'editor:newline' 85 | 'ctrl-/': 'core:undo' 86 | 'ctrl-_': 'core:undo' 87 | 'ctrl-x u': 'core:undo' 88 | 'alt-/': 'atomic-emacs:dabbrev-expand' 89 | 'alt-?': 'atomic-emacs:dabbrev-previous' 90 | 'alt-q': 'autoflow:reflow-selection' 91 | 'alt-;': 'editor:toggle-line-comments' 92 | 'ctrl-alt-\\' : 'editor:auto-indent' 93 | 94 | # Searching 95 | 'ctrl-s': 'atomic-emacs:isearch-forward' 96 | 'ctrl-r': 'atomic-emacs:isearch-backward' 97 | 98 | # Marking & Selecting 99 | 'ctrl-space': 'atomic-emacs:set-mark' 100 | 'ctrl-alt-space': 'atomic-emacs:mark-sexp' 101 | 'ctrl-x h': 'atomic-emacs:mark-whole-buffer' 102 | 'ctrl-x ctrl-x': 'atomic-emacs:exchange-point-and-mark' 103 | 104 | # UI 105 | 'ctrl-g': 'core:cancel' 106 | 'ctrl-x ctrl-s': 'core:save' 107 | 'ctrl-x ctrl-w': 'core:save-as' 108 | 'alt-.': 'symbols-view:go-to-declaration' 109 | 110 | 'atom-text-editor:not(.autocomplete-active):not(.mini)': 111 | # Navigation 112 | 'ctrl-p': 'atomic-emacs:previous-line' 113 | 'up': 'atomic-emacs:previous-line' 114 | 'ctrl-n': 'atomic-emacs:next-line' 115 | 'down': 'atomic-emacs:next-line' 116 | 117 | # Scrolling 118 | 'ctrl-l': 'atomic-emacs:recenter-top-bottom' 119 | 'ctrl-v': 'atomic-emacs:scroll-up' 120 | 'alt-v': 'atomic-emacs:scroll-down' 121 | 122 | # Editing 123 | 'tab': 'editor:auto-indent' 124 | 125 | '.platform-linux atom-workspace.atomic-emacs atom-text-editor': 126 | 'ctrl-d': 'core:delete' 127 | 'ctrl-e': 'editor:move-to-end-of-screen-line' 128 | 129 | '.platform-linux atom-workspace atom-text-editor:not([mini]), 130 | .platform-win32 atom-workspace atom-text-editor:not([mini])': 131 | 'ctrl-j': 'editor:newline' 132 | 133 | '.platform-linux atom-workspace atom-text-editor:not(.autocomplete-active):not([mini]), 134 | .platform-win32 atom-workspace atom-text-editor:not(.autocomplete-active):not([mini])': 135 | 'ctrl-/': 'core:undo' 136 | 137 | '.platform-win32 atom-workspace.atomic-emacs atom-text-editor': 138 | 'ctrl-d': 'core:delete' 139 | 'ctrl-e': 'editor:move-to-end-of-screen-line' 140 | 'ctrl-k ctrl-k': 'editor:cut-to-end-of-line' 141 | 142 | 'atom-workspace .find-and-replace atom-text-editor': 143 | 'ctrl-s': 'find-and-replace:confirm' 144 | 'ctrl-r': 'find-and-replace:show-previous' 145 | 146 | '.go-to-line atom-text-editor[mini] input': 147 | 'ctrl-g': 'core:cancel' 148 | 149 | '.tree-view-dialog atom-text-editor[mini]': 150 | 'ctrl-g': 'core:cancel' 151 | 152 | 'atom-panel.left, atom-panel.right': 153 | 'ctrl-g': 'tool-panel:unfocus' 154 | 155 | '.platform-linux atom-workspace, .platform-win32 atom-workspace': 156 | 'alt-g g': 'go-to-line:toggle' 157 | 'alt-g alt-g': 'go-to-line:toggle' 158 | 'ctrl-g': 'core:cancel' 159 | 160 | '.platform-win32 .go-to-line atom-text-editor[mini] input': 161 | 'ctrl-g': 'core:cancel' 162 | 163 | '.platform-darwin': 164 | 'alt-g g': 'go-to-line:toggle' 165 | 'alt-g alt-g': 'go-to-line:toggle' 166 | 'ctrl-g': 'core:cancel' 167 | 168 | '.platform-darwin .go-to-line atom-text-editor[mini] input': 169 | 'ctrl-g': 'core:cancel' 170 | 171 | 'atom-text-editor.autocomplete-active': 172 | 'ctrl-g': 'autocomplete-plus:cancel' 173 | 174 | 'atom-text-editor !important, atom-text-editor[mini] !important': 175 | 'ctrl-g': 'editor:consolidate-selections' 176 | 177 | 'atom-workspace .atomic-emacs.search atom-text-editor': 178 | 'enter': 'atomic-emacs:isearch-exit' 179 | 'ctrl-m': 'atomic-emacs:isearch-exit' 180 | 'escape': 'atomic-emacs:isearch-cancel' 181 | 'ctrl-g': 'atomic-emacs:isearch-cancel' 182 | 'ctrl-s': 'atomic-emacs:isearch-repeat-forward' 183 | 'ctrl-r': 'atomic-emacs:isearch-repeat-backward' 184 | 'alt-s c': 'atomic-emacs:isearch-toggle-case-fold' 185 | 'alt-c': 'atomic-emacs:isearch-toggle-case-fold' 186 | 'alt-s r': 'atomic-emacs:isearch-toggle-regexp' 187 | 'alt-r': 'atomic-emacs:isearch-toggle-regexp' 188 | 'ctrl-w': 'atomic-emacs:isearch-yank-word-or-character' 189 | -------------------------------------------------------------------------------- /lib/atomic-emacs.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | Completer = require './completer' 3 | EmacsCursor = require './emacs-cursor' 4 | EmacsEditor = require './emacs-editor' 5 | KillRing = require './kill-ring' 6 | Mark = require './mark' 7 | SearchManager = require './search-manager' 8 | State = require './state' 9 | 10 | beforeCommand = (event) -> 11 | State.beforeCommand(event) 12 | 13 | afterCommand = (event) -> 14 | Mark.deactivatePending() 15 | 16 | if State.yankComplete() 17 | emacsEditor = getEditor(event) 18 | for emacsCursor in emacsEditor.getEmacsCursors() 19 | emacsCursor.yankComplete() 20 | 21 | State.afterCommand(event) 22 | 23 | getEditor = (event) -> 24 | # Get editor from the event if possible so we can target mini-editors. 25 | editor = event.target?.closest('atom-text-editor')?.getModel?() ? atom.workspace.getActiveTextEditor() 26 | EmacsEditor.for(editor) 27 | 28 | findFile = (event) -> 29 | haveAOF = atom.packages.isPackageLoaded('advanced-open-file') 30 | useAOF = atom.config.get('atomic-emacs.useAdvancedOpenFile') 31 | if haveAOF and useAOF 32 | atom.commands.dispatch(event.target, 'advanced-open-file:toggle') 33 | else 34 | atom.commands.dispatch(event.target, 'fuzzy-finder:toggle-file-finder') 35 | 36 | closeOtherPanes = (event) -> 37 | container = atom.workspace.getPaneContainers().find((c) => c.getLocation() == 'center') 38 | activePane = container?.getActivePane() 39 | return if not activePane? 40 | for pane in container.getPanes() 41 | unless pane is activePane 42 | pane.close() 43 | 44 | module.exports = 45 | EmacsCursor: EmacsCursor 46 | EmacsEditor: EmacsEditor 47 | KillRing: KillRing 48 | Mark: Mark 49 | SearchManager: SearchManager 50 | State: State 51 | 52 | config: 53 | useAdvancedOpenFile: 54 | type: 'boolean', 55 | default: true, 56 | title: 'Use advanced-open-file for find-file if available' 57 | alwaysUseKillRing: 58 | type: 'boolean', 59 | default: false, 60 | title: 'Use kill ring for built-in copy & cut commands' 61 | killToClipboard: 62 | type: 'boolean', 63 | default: true, 64 | title: 'Send kills to the system clipboard' 65 | yankFromClipboard: 66 | type: 'boolean', 67 | default: false, 68 | title: 'Yank changed text from the system clipboard' 69 | killWholeLine: 70 | type: 'boolean', 71 | default: false, 72 | title: 'Always Kill whole line.' 73 | 74 | activate: -> 75 | if @disposable 76 | console.log "atomic-emacs activated twice -- aborting" 77 | return 78 | 79 | State.initialize() 80 | @search = new SearchManager(plugin: @) 81 | document.getElementsByTagName('atom-workspace')[0]?.classList?.add('atomic-emacs') 82 | @disposable = new CompositeDisposable 83 | @disposable.add atom.commands.onWillDispatch (event) -> beforeCommand(event) 84 | @disposable.add atom.commands.onDidDispatch (event) -> afterCommand(event) 85 | @disposable.add atom.commands.add 'atom-text-editor', 86 | # Navigation 87 | "atomic-emacs:backward-char": (event) -> getEditor(event).backwardChar() 88 | "atomic-emacs:forward-char": (event) -> getEditor(event).forwardChar() 89 | "atomic-emacs:backward-word": (event) -> getEditor(event).backwardWord() 90 | "atomic-emacs:forward-word": (event) -> getEditor(event).forwardWord() 91 | "atomic-emacs:backward-sexp": (event) -> getEditor(event).backwardSexp() 92 | "atomic-emacs:forward-sexp": (event) -> getEditor(event).forwardSexp() 93 | "atomic-emacs:backward-list": (event) -> getEditor(event).backwardList() 94 | "atomic-emacs:forward-list": (event) -> getEditor(event).forwardList() 95 | "atomic-emacs:previous-line": (event) -> getEditor(event).previousLine() 96 | "atomic-emacs:next-line": (event) -> getEditor(event).nextLine() 97 | "atomic-emacs:backward-paragraph": (event) -> getEditor(event).backwardParagraph() 98 | "atomic-emacs:forward-paragraph": (event) -> getEditor(event).forwardParagraph() 99 | "atomic-emacs:back-to-indentation": (event) -> getEditor(event).backToIndentation() 100 | 101 | # Killing & Yanking 102 | "atomic-emacs:backward-kill-word": (event) -> getEditor(event).backwardKillWord() 103 | "atomic-emacs:kill-word": (event) -> getEditor(event).killWord() 104 | "atomic-emacs:kill-line": (event) -> getEditor(event).killLine() 105 | "atomic-emacs:kill-region": (event) -> getEditor(event).killRegion() 106 | "atomic-emacs:copy-region-as-kill": (event) -> getEditor(event).copyRegionAsKill() 107 | "atomic-emacs:append-next-kill": (event) -> State.killed() 108 | "atomic-emacs:yank": (event) -> getEditor(event).yank() 109 | "atomic-emacs:yank-pop": (event) -> getEditor(event).yankPop() 110 | "atomic-emacs:yank-shift": (event) -> getEditor(event).yankShift() 111 | "atomic-emacs:cut": (event) -> 112 | if atom.config.get('atomic-emacs.alwaysUseKillRing') 113 | getEditor(event).killRegion() 114 | else 115 | event.abortKeyBinding() 116 | "atomic-emacs:copy": (event) -> 117 | if atom.config.get('atomic-emacs.alwaysUseKillRing') 118 | getEditor(event).copyRegionAsKill() 119 | else 120 | event.abortKeyBinding() 121 | 122 | # Editing 123 | "atomic-emacs:delete-horizontal-space": (event) -> getEditor(event).deleteHorizontalSpace() 124 | "atomic-emacs:delete-indentation": (event) -> getEditor(event).deleteIndentation() 125 | "atomic-emacs:open-line": (event) -> getEditor(event).openLine() 126 | "atomic-emacs:just-one-space": (event) -> getEditor(event).justOneSpace() 127 | "atomic-emacs:delete-blank-lines": (event) -> getEditor(event).deleteBlankLines() 128 | "atomic-emacs:transpose-chars": (event) -> getEditor(event).transposeChars() 129 | "atomic-emacs:transpose-lines": (event) -> getEditor(event).transposeLines() 130 | "atomic-emacs:transpose-sexps": (event) -> getEditor(event).transposeSexps() 131 | "atomic-emacs:transpose-words": (event) -> getEditor(event).transposeWords() 132 | "atomic-emacs:downcase-word-or-region": (event) -> getEditor(event).downcaseWordOrRegion() 133 | "atomic-emacs:upcase-word-or-region": (event) -> getEditor(event).upcaseWordOrRegion() 134 | "atomic-emacs:capitalize-word-or-region": (event) -> getEditor(event).capitalizeWordOrRegion() 135 | "atomic-emacs:dabbrev-expand": (event) -> getEditor(event).dabbrevExpand() 136 | "atomic-emacs:dabbrev-previous": (event) -> getEditor(event).dabbrevPrevious() 137 | 138 | # Searching 139 | "atomic-emacs:isearch-forward": (event) => @search.start(getEditor(event), direction: 'forward') 140 | "atomic-emacs:isearch-backward": (event) => @search.start(getEditor(event), direction: 'backward') 141 | 142 | # Marking & Selecting 143 | "atomic-emacs:set-mark": (event) -> getEditor(event).setMark() 144 | "atomic-emacs:mark-sexp": (event) -> getEditor(event).markSexp() 145 | "atomic-emacs:mark-whole-buffer": (event) -> getEditor(event).markWholeBuffer() 146 | "atomic-emacs:exchange-point-and-mark": (event) -> getEditor(event).exchangePointAndMark() 147 | 148 | # Scrolling 149 | "atomic-emacs:recenter-top-bottom": (event) -> getEditor(event).recenterTopBottom() 150 | "atomic-emacs:scroll-down": (event) -> getEditor(event).scrollDown() 151 | "atomic-emacs:scroll-up": (event) -> getEditor(event).scrollUp() 152 | 153 | # UI 154 | "core:cancel": (event) -> getEditor(event).keyboardQuit() 155 | 156 | @disposable.add atom.commands.add '.atomic-emacs.search atom-text-editor', 157 | "atomic-emacs:isearch-exit": (event) => @search.exit() 158 | "atomic-emacs:isearch-cancel": (event) => @search.cancel() 159 | "atomic-emacs:isearch-repeat-forward": (event) => @search.repeat('forward') 160 | "atomic-emacs:isearch-repeat-backward": (event) => @search.repeat('backward') 161 | "atomic-emacs:isearch-toggle-case-fold": (event) => @search.toggleCaseSensitivity() 162 | "atomic-emacs:isearch-toggle-regexp": (event) => @search.toggleIsRegExp() 163 | "atomic-emacs:isearch-yank-word-or-character": (event) => @search.yankWordOrCharacter() 164 | 165 | @disposable.add atom.commands.add 'atom-workspace', 166 | "atomic-emacs:find-file": (event) -> findFile(event) 167 | "atomic-emacs:close-other-panes": (event) -> closeOtherPanes(event) 168 | 169 | deactivate: -> 170 | document.getElementsByTagName('atom-workspace')[0]?.classList?.remove('atomic-emacs') 171 | @disposable?.dispose() 172 | @disposable = null 173 | KillRing.global.reset() 174 | @search.destroy() 175 | 176 | consumeElementIcons: (@addIconToElement) -> 177 | 178 | service_0_13: -> 179 | state: State 180 | search: @search 181 | editor: (atomEditor) -> EmacsEditor.for(atomEditor) 182 | cursor: (atomCursor) -> @editor(atomCursor.editor).getEmacsCursorFor(atomCursor) 183 | getEditor: (event) -> getEditor(event) 184 | -------------------------------------------------------------------------------- /lib/completer.coffee: -------------------------------------------------------------------------------- 1 | {Point, Range} = require 'atom' 2 | State = require './state' 3 | Utils = require './utils' 4 | 5 | # Taken from the built-in find-and-replace package (escapeRegExp). 6 | escapeForRegExp = (string) -> 7 | string.replace(/[\/\\^$*+?.()|[\]{}]/g, '\\$&') 8 | 9 | getNonSymbolCharacterRegExp = -> 10 | nonWordCharacters = atom.config.get('editor.nonWordCharacters') 11 | if nonWordCharacters.includes('-') 12 | nonWordCharacters = nonWordCharacters.replace('-', '') + '-' 13 | nonWordCharacters = nonWordCharacters.replace('_', '') 14 | new RegExp('[\\s' + escapeForRegExp(nonWordCharacters) + ']') 15 | 16 | endOfWordPositionFrom = (editor, point) -> 17 | eob = editor.getBuffer().getEndPosition() 18 | result = null 19 | editor.scanInBufferRange getNonSymbolCharacterRegExp(), [point, eob], (hit) -> 20 | result = hit 21 | if result? then result.range.start else eob 22 | 23 | # A stage of the search for completions. 24 | # 25 | # This represents a single pass through a region of text, possibly backwards. 26 | # The search for completions consists of a number of stages: 27 | class Stage 28 | constructor: (@regExp, @editor, @range, @searchBackward, @getNextStage) -> 29 | 30 | scan: -> 31 | result = null 32 | if @searchBackward 33 | @editor.backwardsScanInBufferRange(@regExp, @range, (hit) => 34 | endOfWordPosition = endOfWordPositionFrom(@editor, hit.range.end) 35 | result = @editor.getTextInBufferRange([hit.range.start, endOfWordPosition]) 36 | @range = new Range(@range.start, hit.range.start) 37 | hit.stop() 38 | ) 39 | else 40 | @editor.scanInBufferRange(@regExp, @range, (hit) => 41 | endOfWordPosition = endOfWordPositionFrom(@editor, hit.range.end) 42 | result = @editor.getTextInBufferRange([hit.range.start, endOfWordPosition]) 43 | @range = new Range(hit.range.end, @range.end) 44 | hit.stop() 45 | ) 46 | 47 | if result? 48 | [@, result] 49 | else 50 | [@getNextStage?(), null] 51 | 52 | module.exports = 53 | class Completer 54 | constructor: (@emacsEditor, @emacsCursor) -> 55 | eob = @emacsEditor.editor.getBuffer().getEndPosition() 56 | prefixStart = @emacsCursor.locateBackward(getNonSymbolCharacterRegExp())?.end ? Utils.BOB 57 | prefixEnd = @emacsCursor.locateForward(getNonSymbolCharacterRegExp())?.start ? eob 58 | point = @emacsCursor.cursor.getBufferPosition() 59 | 60 | @_completions = [] 61 | @currentIndex = null 62 | 63 | if prefixStart.isEqual(point) 64 | @_scanningDone = true 65 | return 66 | 67 | @_marker = @emacsEditor.editor.markBufferRange([prefixStart, point]) 68 | @prefix = @emacsEditor.editor.getTextInBufferRange([prefixStart, point]) 69 | 70 | backwardRange = new Range(Utils.BOB, prefixStart) 71 | forwardRange = new Range(prefixEnd, eob) 72 | 73 | regExp = new RegExp("\\b#{escapeForRegExp(@prefix)}") 74 | thisEditor = @emacsEditor.editor 75 | otherEditors = atom.workspace.getTextEditors().filter (editor) -> 76 | editor isnt thisEditor 77 | 78 | # Stages: 79 | # * 1 backward search, from point to the beginning of buffer 80 | # * 1 forward search, from point to the end of the buffer 81 | # * N forward searches, of whole buffers, for each other open file 82 | @_stage = new Stage regExp, thisEditor, backwardRange, true, => 83 | nextEditorStage = (index) => 84 | if index < otherEditors.length - 1 85 | editor = otherEditors[index] 86 | range = new Range(Utils.BOB, editor.getBuffer().getEndPosition()) 87 | => new Stage(regExp, editor, range, false, nextEditorStage(index + 1)) 88 | else 89 | null 90 | new Stage regExp, thisEditor, forwardRange, false, nextEditorStage(0) 91 | 92 | # "native!" commands don't first on{Did,Will}Dispatch, so we need to listen 93 | # to editor changes that occur outside a command. 94 | @disposable = thisEditor.onDidChange => 95 | if not State.isDuringCommand 96 | @emacsEditor.dabbrevDone() 97 | 98 | currentWord = @emacsEditor.editor.getTextInBufferRange([prefixStart, endOfWordPositionFrom(@emacsEditor.editor, point)]) 99 | @_seen = new Set([currentWord]) 100 | @_scanningDone = false 101 | @_loadNextCompletion() 102 | if @_completions.length > 0 103 | @select(0) 104 | 105 | select: (index) -> 106 | @currentIndex = index 107 | @emacsEditor.editor.setTextInBufferRange(@_marker.getBufferRange(), @_completions[index]) 108 | 109 | next: -> 110 | # Bail if there are no completions. 111 | return if @currentIndex is null 112 | 113 | if @currentIndex == @_completions.length - 1 114 | @_loadNextCompletion() 115 | @select((@currentIndex + 1) % @_completions.length) 116 | 117 | previous: -> 118 | # Bail if there are no completions. 119 | return if @currentIndex is null 120 | 121 | if @currentIndex == 0 122 | # If we've been to the end and wrapped around, allow going back. 123 | if @_scanningDone and @_completions.length > 0 124 | @select(@_completions.length - 1) 125 | else 126 | @select(@currentIndex - 1) 127 | 128 | _loadNextCompletion: -> 129 | if @_scanningDone 130 | return null 131 | 132 | while @_stage? 133 | [@_stage, completion] = @_stage.scan() 134 | if completion? and not @_seen.has(completion) 135 | @_completions.push(completion) 136 | @_seen.add(completion) 137 | return null 138 | @_scanningDone = true 139 | null 140 | 141 | destroy: -> 142 | @disposable?.dispose() 143 | -------------------------------------------------------------------------------- /lib/emacs-cursor.coffee: -------------------------------------------------------------------------------- 1 | KillRing = require './kill-ring' 2 | Mark = require './mark' 3 | Utils = require './utils' 4 | {CompositeDisposable} = require 'atom' 5 | 6 | OPENERS = {'(': ')', '[': ']', '{': '}', '\'': '\'', '"': '"', '`': '`'} 7 | CLOSERS = {')': '(', ']': '[', '}': '{', '\'': '\'', '"': '"', '`': '`'} 8 | 9 | module.exports = 10 | class EmacsCursor 11 | @for: (emacsEditor, cursor) -> 12 | cursor._atomicEmacs ?= new EmacsCursor(emacsEditor, cursor) 13 | 14 | constructor: (@emacsEditor, @cursor) -> 15 | @editor = @cursor.editor 16 | @_mark = null 17 | @_localKillRing = null 18 | @_yankMarker = null 19 | @_disposable = @cursor.onDidDestroy => @destroy() 20 | 21 | mark: -> 22 | @_mark ?= new Mark(@cursor) 23 | 24 | killRing: -> 25 | if @editor.hasMultipleCursors() 26 | @getLocalKillRing() 27 | else 28 | KillRing.global 29 | 30 | getLocalKillRing: -> 31 | @_localKillRing ?= KillRing.global.fork() 32 | 33 | clearLocalKillRing: -> 34 | @_localKillRing = null 35 | 36 | destroy: -> 37 | @clearLocalKillRing() 38 | @_disposable.dispose() 39 | @_disposable = null 40 | @_yankMarker?.destroy() 41 | @_mark?.destroy() 42 | delete @cursor._atomicEmacs 43 | 44 | # Look for the previous occurrence of the given regexp. 45 | # 46 | # Return a Range if found, null otherwise. This does not move the cursor. 47 | locateBackward: (regExp) -> 48 | @emacsEditor.locateBackwardFrom(@cursor.getBufferPosition(), regExp) 49 | 50 | # Look for the next occurrence of the given regexp. 51 | # 52 | # Return a Range if found, null otherwise. This does not move the cursor. 53 | locateForward: (regExp) -> 54 | @emacsEditor.locateForwardFrom(@cursor.getBufferPosition(), regExp) 55 | 56 | # Look for the previous word character. 57 | # 58 | # Return a Range if found, null otherwise. This does not move the cursor. 59 | locateWordCharacterBackward: -> 60 | @locateBackward @_getWordCharacterRegExp() 61 | 62 | # Look for the next word character. 63 | # 64 | # Return a Range if found, null otherwise. This does not move the cursor. 65 | locateWordCharacterForward: -> 66 | @locateForward @_getWordCharacterRegExp() 67 | 68 | # Look for the previous nonword character. 69 | # 70 | # Return a Range if found, null otherwise. This does not move the cursor. 71 | locateNonWordCharacterBackward: -> 72 | @locateBackward @_getNonWordCharacterRegExp() 73 | 74 | # Look for the next nonword character. 75 | # 76 | # Return a Range if found, null otherwise. This does not move the cursor. 77 | locateNonWordCharacterForward: -> 78 | @locateForward @_getNonWordCharacterRegExp() 79 | 80 | # Move to the start of the previous occurrence of the given regexp. 81 | # 82 | # Return true if found, false otherwise. 83 | goToMatchStartBackward: (regExp) -> 84 | @_goTo @locateBackward(regExp)?.start 85 | 86 | # Move to the start of the next occurrence of the given regexp. 87 | # 88 | # Return true if found, false otherwise. 89 | goToMatchStartForward: (regExp) -> 90 | @_goTo @locateForward(regExp)?.start 91 | 92 | # Move to the end of the previous occurrence of the given regexp. 93 | # 94 | # Return true if found, false otherwise. 95 | goToMatchEndBackward: (regExp) -> 96 | @_goTo @locateBackward(regExp)?.end 97 | 98 | # Move to the end of the next occurrence of the given regexp. 99 | # 100 | # Return true if found, false otherwise. 101 | goToMatchEndForward: (regExp) -> 102 | @_goTo @locateForward(regExp)?.end 103 | 104 | # Skip backwards over the given characters. 105 | # 106 | # If the end of the buffer is reached, remain there. 107 | skipCharactersBackward: (characters) -> 108 | regexp = new RegExp("[^#{Utils.escapeForRegExp(characters)}]") 109 | @skipBackwardUntil(regexp) 110 | 111 | # Skip forwards over the given characters. 112 | # 113 | # If the end of the buffer is reached, remain there. 114 | skipCharactersForward: (characters) -> 115 | regexp = new RegExp("[^#{Utils.escapeForRegExp(characters)}]") 116 | @skipForwardUntil(regexp) 117 | 118 | # Skip backwards over any word characters. 119 | # 120 | # If the beginning of the buffer is reached, remain there. 121 | skipWordCharactersBackward: -> 122 | @skipBackwardUntil(@_getNonWordCharacterRegExp()) 123 | 124 | # Skip forwards over any word characters. 125 | # 126 | # If the end of the buffer is reached, remain there. 127 | skipWordCharactersForward: -> 128 | @skipForwardUntil(@_getNonWordCharacterRegExp()) 129 | 130 | # Skip backwards over any non-word characters. 131 | # 132 | # If the beginning of the buffer is reached, remain there. 133 | skipNonWordCharactersBackward: -> 134 | @skipBackwardUntil(@_getWordCharacterRegExp()) 135 | 136 | # Skip forwards over any non-word characters. 137 | # 138 | # If the end of the buffer is reached, remain there. 139 | skipNonWordCharactersForward: -> 140 | @skipForwardUntil(@_getWordCharacterRegExp()) 141 | 142 | # Skip over characters until the previous occurrence of the given regexp. 143 | # 144 | # If the beginning of the buffer is reached, remain there. 145 | skipBackwardUntil: (regexp) -> 146 | if not @goToMatchEndBackward(regexp) 147 | @_goTo Utils.BOB 148 | 149 | # Skip over characters until the next occurrence of the given regexp. 150 | # 151 | # If the end of the buffer is reached, remain there. 152 | skipForwardUntil: (regexp) -> 153 | if not @goToMatchStartForward(regexp) 154 | @_goTo @editor.getEofBufferPosition() 155 | 156 | # Insert the given text after this cursor. 157 | insertAfter: (text) -> 158 | position = @cursor.getBufferPosition() 159 | @editor.setTextInBufferRange([position, position], "\n") 160 | @cursor.setBufferPosition(position) 161 | 162 | horizontalSpaceRange: -> 163 | @skipCharactersBackward(' \t') 164 | start = @cursor.getBufferPosition() 165 | @skipCharactersForward(' \t') 166 | end = @cursor.getBufferPosition() 167 | [start, end] 168 | 169 | deleteBlankLines: -> 170 | eof = @editor.getEofBufferPosition() 171 | blankLineRe = /^[ \t]*$/ 172 | 173 | point = @cursor.getBufferPosition() 174 | s = e = point.row 175 | while blankLineRe.test(@cursor.editor.lineTextForBufferRow(e)) and e <= eof.row 176 | e += 1 177 | while s > 0 and blankLineRe.test(@cursor.editor.lineTextForBufferRow(s - 1)) 178 | s -= 1 179 | 180 | if s == e 181 | # No blanks: delete blanks ahead. 182 | e += 1 183 | while blankLineRe.test(@cursor.editor.lineTextForBufferRow(e)) and e <= eof.row 184 | e += 1 185 | @cursor.editor.setTextInBufferRange([[s + 1, 0], [e, 0]], '') 186 | else if e == s + 1 187 | # One blank: delete it. 188 | @cursor.editor.setTextInBufferRange([[s, 0], [e, 0]], '') 189 | @cursor.setBufferPosition([s, 0]) 190 | else 191 | # Multiple blanks: delete all but one. 192 | @cursor.editor.setTextInBufferRange([[s, 0], [e, 0]], '\n') 193 | @cursor.setBufferPosition([s, 0]) 194 | 195 | transformWord: (transformer) -> 196 | @skipNonWordCharactersForward() 197 | start = @cursor.getBufferPosition() 198 | @skipWordCharactersForward() 199 | end = @cursor.getBufferPosition() 200 | range = [start, end] 201 | text = @editor.getTextInBufferRange(range) 202 | @editor.setTextInBufferRange(range, transformer(text)) 203 | 204 | backwardKillWord: (method) -> 205 | @_killUnit method, => 206 | end = @cursor.getBufferPosition() 207 | @skipNonWordCharactersBackward() 208 | @skipWordCharactersBackward() 209 | start = @cursor.getBufferPosition() 210 | [start, end] 211 | 212 | killWord: (method) -> 213 | @_killUnit method, => 214 | start = @cursor.getBufferPosition() 215 | @skipNonWordCharactersForward() 216 | @skipWordCharactersForward() 217 | end = @cursor.getBufferPosition() 218 | [start, end] 219 | 220 | killLine: (method) -> 221 | @_killUnit method, => 222 | start = @cursor.getBufferPosition() 223 | line = @editor.lineTextForBufferRow(start.row) 224 | if start.column == 0 and atom.config.get("atomic-emacs.killWholeLine") 225 | end = [start.row + 1, 0] 226 | else 227 | if /^\s*$/.test(line.slice(start.column)) 228 | end = [start.row + 1, 0] 229 | else 230 | end = [start.row, line.length] 231 | [start, end] 232 | 233 | killRegion: (method) -> 234 | @_killUnit method, => 235 | position = @cursor.selection.getBufferRange() 236 | [position, position] 237 | 238 | _killUnit: (method='push', findRange) -> 239 | if @cursor.selection? and not @cursor.selection.isEmpty() 240 | range = @cursor.selection.getBufferRange() 241 | @cursor.selection.clear() 242 | else 243 | range = findRange() 244 | 245 | text = @editor.getTextInBufferRange(range) 246 | @editor.setTextInBufferRange(range, '') 247 | killRing = @killRing() 248 | killRing[method](text) 249 | killRing.getCurrentEntry() 250 | 251 | yank: -> 252 | killRing = @killRing() 253 | return if killRing.isEmpty() 254 | if @cursor.selection 255 | range = @cursor.selection.getBufferRange() 256 | @cursor.selection.clear() 257 | else 258 | position = @cursor.getBufferPosition() 259 | range = [position, position] 260 | newRange = @editor.setTextInBufferRange(range, killRing.getCurrentEntry()) 261 | @cursor.setBufferPosition(newRange.end) 262 | @_yankMarker ?= @editor.markBufferPosition(@cursor.getBufferPosition()) 263 | @_yankMarker.setBufferRange(newRange) 264 | 265 | rotateYank: (n) -> 266 | return if @_yankMarker == null 267 | entry = @killRing().rotate(n) 268 | unless entry is null 269 | range = @editor.setTextInBufferRange(@_yankMarker.getBufferRange(), entry) 270 | @_yankMarker.setBufferRange(range) 271 | 272 | yankComplete: -> 273 | @_yankMarker?.destroy() 274 | @_yankMarker = null 275 | 276 | nextCharacter: -> 277 | @emacsEditor.characterAfter(@cursor.getBufferPosition()) 278 | 279 | # Skip to the end of the current or next symbolic expression. 280 | skipSexpForward: -> 281 | point = @cursor.getBufferPosition() 282 | target = @_sexpForwardFrom(point) 283 | @cursor.setBufferPosition(target) 284 | 285 | # Skip to the beginning of the current or previous symbolic expression. 286 | skipSexpBackward: -> 287 | point = @cursor.getBufferPosition() 288 | target = @_sexpBackwardFrom(point) 289 | @cursor.setBufferPosition(target) 290 | 291 | # Skip to the end of the current or next list. 292 | skipListForward: -> 293 | point = @cursor.getBufferPosition() 294 | target = @_listForwardFrom(point) 295 | @cursor.setBufferPosition(target) if target 296 | 297 | # Skip to the beginning of the current or previous list. 298 | skipListBackward: -> 299 | point = @cursor.getBufferPosition() 300 | target = @_listBackwardFrom(point) 301 | @cursor.setBufferPosition(target) if target 302 | 303 | # Add the next sexp to the cursor's selection. Activate if necessary. 304 | markSexp: -> 305 | range = @cursor.getMarker().getBufferRange() 306 | newTail = @_sexpForwardFrom(range.end) 307 | mark = @mark().set(newTail) 308 | mark.activate() unless mark.isActive() 309 | 310 | # Transpose the two characters around the cursor. At the beginning of a line, 311 | # transpose the newline with the first character of the line. At the end of a 312 | # line, transpose the last two characters. At the beginning of the buffer, do 313 | # nothing. Weird, but that's Emacs! 314 | transposeChars: -> 315 | {row, column} = @cursor.getBufferPosition() 316 | return if row == 0 and column == 0 317 | 318 | line = @editor.lineTextForBufferRow(row) 319 | if column == 0 320 | previousLine = @editor.lineTextForBufferRow(row - 1) 321 | pairRange = [[row - 1, previousLine.length], [row, 1]] 322 | else if column == line.length 323 | pairRange = [[row, column - 2], [row, column]] 324 | else 325 | pairRange = [[row, column - 1], [row, column + 1]] 326 | pair = @editor.getTextInBufferRange(pairRange) 327 | @editor.setTextInBufferRange(pairRange, (pair[1] or '') + pair[0]) 328 | 329 | # Transpose the word at the cursor with the next one. Move to the end of the 330 | # next word. 331 | transposeWords: -> 332 | @skipNonWordCharactersBackward() 333 | 334 | word1Range = @_wordRange() 335 | @skipWordCharactersForward() 336 | @skipNonWordCharactersForward() 337 | if @editor.getEofBufferPosition().isEqual(@cursor.getBufferPosition()) 338 | # No second word - just go back. 339 | @skipNonWordCharactersBackward() 340 | else 341 | word2Range = @_wordRange() 342 | @_transposeRanges(word1Range, word2Range) 343 | 344 | # Transpose the sexp at the cursor with the next one. Move to the end of the 345 | # next sexp. 346 | transposeSexps: -> 347 | @skipSexpBackward() 348 | start1 = @cursor.getBufferPosition() 349 | @skipSexpForward() 350 | end1 = @cursor.getBufferPosition() 351 | 352 | @skipSexpForward() 353 | end2 = @cursor.getBufferPosition() 354 | @skipSexpBackward() 355 | start2 = @cursor.getBufferPosition() 356 | 357 | @_transposeRanges([start1, end1], [start2, end2]) 358 | 359 | # Transpose the line at the cursor with the one above it. Move to the 360 | # beginning of the next line. 361 | transposeLines: -> 362 | row = @cursor.getBufferRow() 363 | if row == 0 364 | @_endLineIfNecessary() 365 | @cursor.moveDown() 366 | row += 1 367 | @_endLineIfNecessary() 368 | 369 | lineRange = [[row, 0], [row + 1, 0]] 370 | text = @editor.getTextInBufferRange(lineRange) 371 | @editor.setTextInBufferRange(lineRange, '') 372 | @editor.setTextInBufferRange([[row - 1, 0], [row - 1, 0]], text) 373 | 374 | _wordRange: -> 375 | @skipWordCharactersBackward() 376 | range = @locateNonWordCharacterBackward() 377 | wordStart = if range then range.end else [0, 0] 378 | range = @locateNonWordCharacterForward() 379 | wordEnd = if range then range.start else @editor.getEofBufferPosition() 380 | [wordStart, wordEnd] 381 | 382 | _endLineIfNecessary: -> 383 | row = @cursor.getBufferPosition().row 384 | if row == @editor.getLineCount() - 1 385 | length = @cursor.getCurrentBufferLine().length 386 | @editor.setTextInBufferRange([[row, length], [row, length]], "\n") 387 | 388 | _transposeRanges: (range1, range2) -> 389 | text1 = @editor.getTextInBufferRange(range1) 390 | text2 = @editor.getTextInBufferRange(range2) 391 | 392 | # Update range2 first so it doesn't change range1. 393 | @editor.setTextInBufferRange(range2, text1) 394 | @editor.setTextInBufferRange(range1, text2) 395 | @cursor.setBufferPosition(range2[1]) 396 | 397 | _sexpForwardFrom: (point) -> 398 | eob = @editor.getEofBufferPosition() 399 | point = @emacsEditor.locateForwardFrom(point, /[\w()[\]{}'"]/i)?.start or eob 400 | character = @emacsEditor.characterAfter(point) 401 | if OPENERS.hasOwnProperty(character) or CLOSERS.hasOwnProperty(character) 402 | result = null 403 | stack = [] 404 | quotes = 0 405 | eof = @editor.getEofBufferPosition() 406 | re = /[^()[\]{}"'`\\]+|\\.|[()[\]{}"'`]/g 407 | @editor.scanInBufferRange re, [point, eof], (hit) => 408 | if hit.matchText == stack[stack.length - 1] 409 | stack.pop() 410 | if stack.length == 0 411 | result = hit.range.end 412 | hit.stop() 413 | else if /^["'`]$/.test(hit.matchText) 414 | quotes -= 1 415 | else if (closer = OPENERS[hit.matchText]) 416 | unless /^["'`]$/.test(closer) and quotes > 0 417 | stack.push(closer) 418 | quotes += 1 if /^["'`]$/.test(closer) 419 | else if CLOSERS[hit.matchText] 420 | if stack.length == 0 421 | hit.stop() 422 | result or point 423 | else 424 | @emacsEditor.locateForwardFrom(point, /[\W\n]/i)?.start or eob 425 | 426 | _sexpBackwardFrom: (point) -> 427 | point = @emacsEditor.locateBackwardFrom(point, /[\w()[\]{}'"]/i)?.end or Utils.BOB 428 | character = @emacsEditor.characterBefore(point) 429 | if OPENERS.hasOwnProperty(character) or CLOSERS.hasOwnProperty(character) 430 | result = null 431 | stack = [] 432 | quotes = 0 433 | re = /[^()[\]{}"'`\\]+|\\.|[()[\]{}"'`]/g 434 | @editor.backwardsScanInBufferRange re, [Utils.BOB, point], (hit) => 435 | if hit.matchText == stack[stack.length - 1] 436 | stack.pop() 437 | if stack.length == 0 438 | result = hit.range.start 439 | hit.stop() 440 | else if /^["'`]$/.test(hit.matchText) 441 | quotes -= 1 442 | else if (opener = CLOSERS[hit.matchText]) 443 | unless /^["'`]$/.test(opener) and quotes > 0 444 | stack.push(opener) 445 | quotes += 1 if /^["'`]$/.test(opener) 446 | else if OPENERS[hit.matchText] 447 | if stack.length == 0 448 | hit.stop() 449 | result or point 450 | else 451 | @emacsEditor.locateBackwardFrom(point, /[\W\n]/i)?.end or Utils.BOB 452 | 453 | _listForwardFrom: (point) -> 454 | eob = @editor.getEofBufferPosition() 455 | if !(match = @emacsEditor.locateForwardFrom(point, /[()[\]{}]/i)) 456 | return null 457 | end = this._sexpForwardFrom(match.start) 458 | if end.isEqual(match.start) then null else end 459 | 460 | _listBackwardFrom: (point) -> 461 | if !(match = @emacsEditor.locateBackwardFrom(point, /[()[\]{}]/i)) 462 | return null 463 | start = this._sexpBackwardFrom(match.end) 464 | if start.isEqual(match.end) then null else start 465 | 466 | _getWordCharacterRegExp: -> 467 | nonWordCharacters = atom.config.get('editor.nonWordCharacters') 468 | new RegExp('[^\\s' + Utils.escapeForRegExp(nonWordCharacters) + ']') 469 | 470 | _getNonWordCharacterRegExp: -> 471 | nonWordCharacters = atom.config.get('editor.nonWordCharacters') 472 | new RegExp('[\\s' + Utils.escapeForRegExp(nonWordCharacters) + ']') 473 | 474 | _goTo: (point) -> 475 | if point 476 | @cursor.setBufferPosition(point) 477 | true 478 | else 479 | false 480 | -------------------------------------------------------------------------------- /lib/emacs-editor.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Point} = require 'atom' 2 | Completer = require './completer' 3 | EmacsCursor = require './emacs-cursor' 4 | KillRing = require './kill-ring' 5 | Mark = require './mark' 6 | State = require './state' 7 | Utils = require './utils' 8 | 9 | module.exports = 10 | class EmacsEditor 11 | @for: (editor) -> 12 | editor._atomicEmacs ?= new EmacsEditor(editor) 13 | 14 | constructor: (@editor) -> 15 | @disposable = new CompositeDisposable 16 | @disposable.add @editor.onDidRemoveCursor => 17 | cursors = @editor.getCursors() 18 | if cursors.length == 1 19 | @getEmacsCursorFor(cursors[0]).clearLocalKillRing() 20 | @disposable.add @editor.onDidDestroy => 21 | @destroy() 22 | 23 | destroy: -> 24 | # Neither cursor.did-destroy nor TextEditor.did-remove-cursor seems to fire 25 | # when the editor is destroyed. (Atom bug?) So we destroy EmacsCursors here. 26 | for cursor in @getEmacsCursors() 27 | cursor.destroy() 28 | @disposable.dispose() 29 | 30 | getEmacsCursorFor: (cursor) -> 31 | EmacsCursor.for(this, cursor) 32 | 33 | getEmacsCursors: () -> 34 | @getEmacsCursorFor(c) for c in @editor.getCursors() 35 | 36 | moveEmacsCursors: (callback) -> 37 | @editor.moveCursors (cursor) => 38 | # Atom bug: if moving one cursor destroys another, the destroyed one's 39 | # emitter is disposed, but cursor.isDestroyed() is still false. However 40 | # cursor.destroyed == true. TextEditor.moveCursors probably shouldn't even 41 | # yield it in this case. 42 | return if cursor.destroyed == true 43 | callback(@getEmacsCursorFor(cursor), cursor) 44 | 45 | saveCursors: -> 46 | @getEmacsCursors().map (emacsCursor) -> 47 | head: emacsCursor.cursor.marker.getHeadBufferPosition() 48 | tail: emacsCursor.cursor.marker.getTailBufferPosition() or 49 | emacsCursor.cursor.marker.getHeadBufferPosition() 50 | # Atom doesn't have a public API to add a selection to a cursor, so assume 51 | # that an active selection means an active mark. 52 | markActive: emacsCursor.mark().isActive() or 53 | (emacsCursor.cursor.selection and not emacsCursor.cursor.selection.isEmpty()) 54 | 55 | restoreCursors: (selections) -> 56 | cursors = @editor.getCursors() 57 | selections.forEach (info, index) => 58 | point = if info.markActive then info.tail else info.head 59 | if index >= cursors.length 60 | cursor = @editor.addCursorAtBufferPosition(point) 61 | else 62 | cursor = cursors[index] 63 | cursor.setBufferPosition(point) 64 | 65 | emacsCursor = @getEmacsCursorFor(cursor) 66 | if info.markActive 67 | emacsCursor.mark().set().activate() 68 | emacsCursor._goTo(info.head) 69 | 70 | positionAfter: (point) -> 71 | lineLength = @editor.lineTextForBufferRow(point.row).length 72 | if point.column == lineLength 73 | if point.row == @editor.getLastBufferRow() 74 | null 75 | else 76 | new Point(point.row + 1, 0) 77 | else 78 | point.translate([0, 1]) 79 | 80 | positionBefore: (point) -> 81 | if point.column == 0 82 | if point.row == 0 83 | null 84 | else 85 | column = @editor.lineTextForBufferRow(point.row - 1).length 86 | new Point(point.row - 1, column) 87 | else 88 | point.translate([0, -1]) 89 | 90 | characterAfter: (point) -> 91 | p = @positionAfter(point) 92 | if p then @editor.getTextInBufferRange([point, p]) else null 93 | 94 | characterBefore: (point) -> 95 | p = @positionBefore(point) 96 | if p then @editor.getTextInBufferRange([p, point]) else null 97 | 98 | locateBackwardFrom: (point, regExp) -> 99 | result = null 100 | @editor.backwardsScanInBufferRange regExp, [Utils.BOB, point], (hit) -> 101 | result = hit.range 102 | result 103 | 104 | locateForwardFrom: (point, regExp) -> 105 | result = null 106 | eof = @editor.getEofBufferPosition() 107 | @editor.scanInBufferRange regExp, [point, eof], (hit) -> 108 | result = hit.range 109 | result 110 | 111 | ### 112 | Section: Navigation 113 | ### 114 | 115 | backwardChar: -> 116 | @editor.moveCursors (cursor) -> 117 | cursor.moveLeft() 118 | 119 | forwardChar: -> 120 | @editor.moveCursors (cursor) -> 121 | cursor.moveRight() 122 | 123 | backwardWord: -> 124 | @moveEmacsCursors (emacsCursor) -> 125 | emacsCursor.skipNonWordCharactersBackward() 126 | emacsCursor.skipWordCharactersBackward() 127 | 128 | forwardWord: -> 129 | @moveEmacsCursors (emacsCursor) -> 130 | emacsCursor.skipNonWordCharactersForward() 131 | emacsCursor.skipWordCharactersForward() 132 | 133 | backwardSexp: -> 134 | @moveEmacsCursors (emacsCursor) -> 135 | emacsCursor.skipSexpBackward() 136 | 137 | forwardSexp: -> 138 | @moveEmacsCursors (emacsCursor) -> 139 | emacsCursor.skipSexpForward() 140 | 141 | backwardList: -> 142 | @moveEmacsCursors (emacsCursor) -> 143 | emacsCursor.skipListBackward() 144 | 145 | forwardList: -> 146 | @moveEmacsCursors (emacsCursor) -> 147 | emacsCursor.skipListForward() 148 | 149 | previousLine: -> 150 | @editor.moveCursors (cursor) -> 151 | cursor.moveUp() 152 | 153 | nextLine: -> 154 | @editor.moveCursors (cursor) -> 155 | cursor.moveDown() 156 | 157 | backwardParagraph: -> 158 | @moveEmacsCursors (emacsCursor, cursor) -> 159 | position = cursor.getBufferPosition() 160 | unless position.row == 0 161 | cursor.setBufferPosition([position.row - 1, 0]) 162 | 163 | emacsCursor.goToMatchStartBackward(/^\s*$/) or 164 | cursor.moveToTop() 165 | 166 | forwardParagraph: -> 167 | lastRow = @editor.getLastBufferRow() 168 | @moveEmacsCursors (emacsCursor, cursor) -> 169 | position = cursor.getBufferPosition() 170 | unless position.row == lastRow 171 | cursor.setBufferPosition([position.row + 1, 0]) 172 | 173 | emacsCursor.goToMatchStartForward(/^\s*$/) or 174 | cursor.moveToBottom() 175 | 176 | backToIndentation: -> 177 | @editor.moveCursors (cursor) => 178 | position = cursor.getBufferPosition() 179 | line = @editor.lineTextForBufferRow(position.row) 180 | targetColumn = line.search(/\S/) 181 | targetColumn = line.length if targetColumn == -1 182 | 183 | if position.column != targetColumn 184 | cursor.setBufferPosition([position.row, targetColumn]) 185 | 186 | ### 187 | Section: Killing & Yanking 188 | ### 189 | 190 | backwardKillWord: -> 191 | @_pullFromClipboard() 192 | method = if State.killing then 'prepend' else 'push' 193 | kills = [] 194 | @editor.transact => 195 | @moveEmacsCursors (emacsCursor, cursor) => 196 | kill = emacsCursor.backwardKillWord(method) 197 | kills.push(kill) 198 | @_updateGlobalKillRing(method, kills) 199 | State.killed() 200 | 201 | killWord: -> 202 | @_pullFromClipboard() 203 | method = if State.killing then 'append' else 'push' 204 | kills = [] 205 | @editor.transact => 206 | @moveEmacsCursors (emacsCursor) => 207 | kill = emacsCursor.killWord(method) 208 | kills.push(kill) 209 | @_updateGlobalKillRing(method, kills) 210 | State.killed() 211 | 212 | killLine: -> 213 | @_pullFromClipboard() 214 | method = if State.killing then 'append' else 'push' 215 | kills = [] 216 | @editor.transact => 217 | @moveEmacsCursors (emacsCursor) => 218 | kill = emacsCursor.killLine(method) 219 | kills.push(kill) 220 | @_updateGlobalKillRing(method, kills) 221 | State.killed() 222 | 223 | killRegion: -> 224 | @_pullFromClipboard() 225 | method = if State.killing then 'append' else 'push' 226 | kills = [] 227 | @editor.transact => 228 | @moveEmacsCursors (emacsCursor) => 229 | kill = emacsCursor.killRegion(method) 230 | kills.push(kill) 231 | @_updateGlobalKillRing(method, kills) 232 | State.killed() 233 | 234 | copyRegionAsKill: -> 235 | @_pullFromClipboard() 236 | method = if State.killing then 'append' else 'push' 237 | kills = [] 238 | @editor.transact => 239 | for selection in @editor.getSelections() 240 | emacsCursor = @getEmacsCursorFor(selection.cursor) 241 | text = selection.getText() 242 | emacsCursor.killRing()[method](text) 243 | emacsCursor.killRing().getCurrentEntry() 244 | emacsCursor.mark().deactivate() 245 | kills.push(text) 246 | @_updateGlobalKillRing(method, kills) 247 | 248 | yank: -> 249 | @_pullFromClipboard() 250 | @editor.transact => 251 | for emacsCursor in @getEmacsCursors() 252 | emacsCursor.yank() 253 | State.yanked() 254 | 255 | yankPop: -> 256 | return if not State.yanking 257 | @_pullFromClipboard() 258 | @editor.transact => 259 | for emacsCursor in @getEmacsCursors() 260 | emacsCursor.rotateYank(-1) 261 | State.yanked() 262 | 263 | yankShift: -> 264 | return if not State.yanking 265 | @_pullFromClipboard() 266 | @editor.transact => 267 | for emacsCursor in @getEmacsCursors() 268 | emacsCursor.rotateYank(1) 269 | State.yanked() 270 | 271 | _pushToClipboard: -> 272 | if atom.config.get("atomic-emacs.killToClipboard") 273 | KillRing.pushToClipboard() 274 | 275 | _pullFromClipboard: -> 276 | if atom.config.get("atomic-emacs.yankFromClipboard") 277 | killRings = (c.killRing() for c in @getEmacsCursors()) 278 | KillRing.pullFromClipboard(killRings) 279 | 280 | _updateGlobalKillRing: (method, kills) -> 281 | if kills.length > 1 282 | method = 'replace' if method != 'push' 283 | KillRing.global[method](kills.join('\n') + '\n') 284 | @_pushToClipboard() 285 | 286 | ### 287 | Section: Editing 288 | ### 289 | 290 | deleteHorizontalSpace: -> 291 | @editor.transact => 292 | @moveEmacsCursors (emacsCursor) => 293 | range = emacsCursor.horizontalSpaceRange() 294 | @editor.setTextInBufferRange(range, '') 295 | 296 | deleteIndentation: -> 297 | return unless @editor 298 | @editor.transact => 299 | @editor.moveUp() 300 | @editor.joinLines() 301 | 302 | openLine: -> 303 | @editor.transact => 304 | for emacsCursor in @getEmacsCursors() 305 | emacsCursor.insertAfter("\n") 306 | 307 | justOneSpace: -> 308 | @editor.transact => 309 | for emacsCursor in @getEmacsCursors() 310 | range = emacsCursor.horizontalSpaceRange() 311 | @editor.setTextInBufferRange(range, ' ') 312 | 313 | deleteBlankLines: -> 314 | @editor.transact => 315 | for emacsCursor in @getEmacsCursors() 316 | emacsCursor.deleteBlankLines() 317 | 318 | transposeChars: -> 319 | @editor.transact => 320 | @moveEmacsCursors (emacsCursor) => 321 | emacsCursor.transposeChars() 322 | 323 | transposeWords: -> 324 | @editor.transact => 325 | @moveEmacsCursors (emacsCursor) => 326 | emacsCursor.transposeWords() 327 | 328 | transposeLines: -> 329 | @editor.transact => 330 | @moveEmacsCursors (emacsCursor) => 331 | emacsCursor.transposeLines() 332 | 333 | transposeSexps: -> 334 | @editor.transact => 335 | @moveEmacsCursors (emacsCursor) => 336 | emacsCursor.transposeSexps() 337 | 338 | downcase = (s) -> s.toLowerCase() 339 | upcase = (s) -> s.toUpperCase() 340 | capitalize = (s) -> s.slice(0, 1).toUpperCase() + s.slice(1).toLowerCase() 341 | 342 | downcaseWordOrRegion: -> 343 | @_transformWordOrRegion(downcase) 344 | 345 | upcaseWordOrRegion: -> 346 | @_transformWordOrRegion(upcase) 347 | 348 | capitalizeWordOrRegion: -> 349 | @_transformWordOrRegion(capitalize, wordAtATime: true) 350 | 351 | _transformWordOrRegion: (transformWord, {wordAtATime}={}) -> 352 | @editor.transact => 353 | if @editor.getSelections().filter((s) -> not s.isEmpty()).length > 0 354 | @editor.mutateSelectedText (selection) => 355 | range = selection.getBufferRange() 356 | if wordAtATime 357 | @editor.scanInBufferRange /\w+/g, range, (hit) -> 358 | hit.replace(transformWord(hit.matchText)) 359 | else 360 | @editor.setTextInBufferRange(range, transformWord(selection.getText())) 361 | else 362 | for cursor in @editor.getCursors() 363 | cursor.emitter.__track = true 364 | @moveEmacsCursors (emacsCursor) => 365 | emacsCursor.transformWord(transformWord) 366 | 367 | dabbrevExpand: -> 368 | if @completers? 369 | @completers.forEach (completer) -> 370 | completer.next() 371 | else 372 | @editor.transact => 373 | @completers = [] 374 | @moveEmacsCursors (emacsCursor) => 375 | completer = new Completer(@, emacsCursor) 376 | @completers.push(completer) 377 | 378 | State.dabbrevState = {emacsEditor: @} 379 | 380 | dabbrevPrevious: -> 381 | if @completers? 382 | @completers.forEach (completer) -> 383 | completer.previous() 384 | 385 | dabbrevDone: -> 386 | @completers?.forEach (completer) -> 387 | completer.destroy() 388 | @completers = null 389 | 390 | ### 391 | Section: Marking & Selecting 392 | ### 393 | 394 | setMark: -> 395 | for emacsCursor in @getEmacsCursors() 396 | emacsCursor.mark().set().activate() 397 | 398 | markSexp: -> 399 | @moveEmacsCursors (emacsCursor) -> 400 | emacsCursor.markSexp() 401 | 402 | markWholeBuffer: -> 403 | [first, rest...] = @editor.getCursors() 404 | c.destroy() for c in rest 405 | emacsCursor = @getEmacsCursorFor(first) 406 | first.moveToBottom() 407 | emacsCursor.mark().set().activate() 408 | first.moveToTop() 409 | 410 | exchangePointAndMark: -> 411 | @moveEmacsCursors (emacsCursor) -> 412 | emacsCursor.mark().exchange() 413 | 414 | ### 415 | Section: UI 416 | ### 417 | 418 | recenterTopBottom: -> 419 | return unless @editor 420 | view = atom.views.getView(@editor) 421 | minRow = Math.min((c.getBufferRow() for c in @editor.getCursors())...) 422 | maxRow = Math.max((c.getBufferRow() for c in @editor.getCursors())...) 423 | minOffset = view.pixelPositionForBufferPosition([minRow, 0]) 424 | maxOffset = view.pixelPositionForBufferPosition([maxRow, 0]) 425 | 426 | switch State.recenters 427 | when 0 428 | view.setScrollTop((minOffset.top + maxOffset.top - view.getHeight())/2) 429 | when 1 430 | # Atom applies a (hardcoded) 2-line buffer while scrolling -- do that here. 431 | view.setScrollTop(minOffset.top - 2*@editor.getLineHeightInPixels()) 432 | when 2 433 | view.setScrollTop(maxOffset.top + 3*@editor.getLineHeightInPixels() - view.getHeight()) 434 | 435 | State.recentered() 436 | 437 | scrollUp: -> 438 | if (visibleRowRange = @editor.getVisibleRowRange()) 439 | # IF the buffer is empty, we get NaNs here (Atom 1.21). 440 | return unless visibleRowRange.every((e) => !Number.isNaN(e)) 441 | 442 | [firstRow, lastRow] = visibleRowRange 443 | currentRow = @editor.cursors[0].getBufferRow() 444 | rowCount = (lastRow - firstRow) - 2 445 | @editor.moveDown(rowCount) 446 | 447 | scrollDown: -> 448 | if (visibleRowRange = @editor.getVisibleRowRange()) 449 | # IF the buffer is empty, we get NaNs here (Atom 1.21). 450 | return unless visibleRowRange.every((e) => !Number.isNaN(e)) 451 | 452 | [firstRow,lastRow] = visibleRowRange 453 | currentRow = @editor.cursors[0].getBufferRow() 454 | rowCount = (lastRow - firstRow) - 2 455 | @editor.moveUp(rowCount) 456 | 457 | ### 458 | Section: Other 459 | ### 460 | 461 | keyboardQuit: -> 462 | for emacsCursor in @getEmacsCursors() 463 | emacsCursor.mark().deactivate() 464 | -------------------------------------------------------------------------------- /lib/kill-ring.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | class KillRing 3 | constructor: -> 4 | @currentIndex = -1 5 | @entries = [] 6 | @limit = 500 7 | @lastClip = undefined 8 | 9 | fork: -> 10 | fork = new KillRing 11 | fork.setEntries(@entries) 12 | fork.currentIndex = @currentIndex 13 | fork.lastClip = @lastClip 14 | fork 15 | 16 | isEmpty: -> 17 | @entries.length == 0 18 | 19 | reset: -> 20 | @entries = [] 21 | 22 | getEntries: -> 23 | @entries.slice() 24 | 25 | setEntries: (entries) -> 26 | @entries = entries.slice() 27 | @currentIndex = @entries.length - 1 28 | this 29 | 30 | push: (text) -> 31 | @entries.push(text) 32 | if @entries.length > @limit 33 | @entries.shift() 34 | @currentIndex = @entries.length - 1 35 | 36 | append: (text) -> 37 | if @entries.length == 0 38 | @push(text) 39 | else 40 | index = @entries.length - 1 41 | @entries[index] = @entries[index] + text 42 | @currentIndex = @entries.length - 1 43 | 44 | prepend: (text) -> 45 | if @entries.length == 0 46 | @push(text) 47 | else 48 | index = @entries.length - 1 49 | @entries[index] = "#{text}#{@entries[index]}" 50 | @currentIndex = @entries.length - 1 51 | 52 | replace: (text) -> 53 | if @entries.length == 0 54 | @push(text) 55 | else 56 | index = @entries.length - 1 57 | @entries[index] = text 58 | @currentIndex = @entries.length - 1 59 | 60 | getCurrentEntry: -> 61 | if @entries.length == 0 62 | return null 63 | else 64 | @entries[@currentIndex] 65 | 66 | rotate: (n) -> 67 | return null if @entries.length == 0 68 | @currentIndex = (@currentIndex + n) % @entries.length 69 | @currentIndex += @entries.length if @currentIndex < 0 70 | return @entries[@currentIndex] 71 | 72 | @global = new KillRing 73 | 74 | @pullFromClipboard: (killRings) -> 75 | text = atom.clipboard.read() 76 | if text != KillRing.lastClip 77 | KillRing.global.push(text) 78 | KillRing.lastClip = text 79 | if killRings.length > 1 80 | entries = text.split(/\r?\n/) 81 | killRings.forEach (killRing, i) -> 82 | entry = entries[i] ? '' 83 | killRing.push(entry) 84 | 85 | @pushToClipboard: -> 86 | text = KillRing.global.getCurrentEntry() 87 | atom.clipboard.write(text) 88 | KillRing.lastClip = text 89 | -------------------------------------------------------------------------------- /lib/mark.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Point} = require 'atom' 2 | State = require './state' 3 | 4 | # Represents an Emacs-style mark. 5 | # 6 | # Each cursor may have a Mark. On construction, the mark is at the cursor's 7 | # position. 8 | # 9 | # The mark can then be set() at any time, which will move it to where the cursor 10 | # is. 11 | # 12 | # It can also be activate()d and deactivate()d. While active, the region between 13 | # the mark and the cursor is selected, and this selection is updated as the 14 | # cursor is moved. If the buffer is edited, the mark is automatically 15 | # deactivated. 16 | class Mark 17 | @deactivatable = [] 18 | 19 | @deactivatePending: -> 20 | for mark in @deactivatable 21 | mark.deactivate() 22 | @deactivatable.length = 0 23 | 24 | constructor: (cursor) -> 25 | @cursor = cursor 26 | @editor = cursor.editor 27 | @marker = @editor.markBufferPosition(cursor.getBufferPosition()) 28 | @active = false 29 | @updating = false 30 | 31 | destroy: -> 32 | @deactivate() if @active 33 | @marker.destroy() 34 | 35 | set: (point=@cursor.getBufferPosition()) -> 36 | @deactivate() 37 | @marker.setHeadBufferPosition(point) 38 | @_updateSelection() 39 | @ 40 | 41 | getBufferPosition: -> 42 | @marker.getHeadBufferPosition() 43 | 44 | activate: -> 45 | if not @active 46 | @activeSubscriptions = new CompositeDisposable 47 | @activeSubscriptions.add @cursor.onDidChangePosition (event) => 48 | @_updateSelection(event) 49 | # Cursor movement commands like cursor.moveDown deactivate the selection 50 | # unconditionally, but don't trigger onDidChangePosition if the position 51 | # doesn't change (e.g. at EOF). So we also update the selection after any 52 | # command. 53 | @activeSubscriptions.add atom.commands.onDidDispatch (event) => 54 | @_updateSelection(event) 55 | @activeSubscriptions.add @editor.getBuffer().onDidChange (event) => 56 | unless @_isIndent(event) or @_isOutdent(event) 57 | # If we're in a command (as opposed to a simple character insert), 58 | # delay the deactivation until the end of the command. Otherwise 59 | # updating one selection may prematurely deactivate the mark and clear 60 | # a second selection before it has a chance to be updated. 61 | if State.isDuringCommand 62 | Mark.deactivatable.push(this) 63 | else 64 | @deactivate() 65 | @active = true 66 | 67 | deactivate: -> 68 | if @active 69 | @activeSubscriptions.dispose() 70 | @active = false 71 | unless @cursor.editor.isDestroyed() 72 | @cursor.clearSelection() 73 | 74 | isActive: -> 75 | @active 76 | 77 | exchange: -> 78 | position = @marker.getHeadBufferPosition() 79 | @set().activate() 80 | @cursor.setBufferPosition(position) 81 | 82 | _updateSelection: (event) -> 83 | # Updating the selection updates the cursor marker, so guard against the 84 | # nested invocation. 85 | if !@updating 86 | @updating = true 87 | try 88 | head = @cursor.getBufferPosition() 89 | tail = @marker.getHeadBufferPosition() 90 | @setSelectionRange(head, tail) 91 | finally 92 | @updating = false 93 | 94 | getSelectionRange: -> 95 | @cursor.selection.getBufferRange() 96 | 97 | setSelectionRange: (head, tail) -> 98 | reversed = Point.min(head, tail) is head 99 | @cursor.selection.setBufferRange([head, tail], reversed: reversed) 100 | 101 | _isIndent: (event)-> 102 | @_isIndentOutdent(event.newRange, event.newText) 103 | 104 | _isOutdent: (event)-> 105 | @_isIndentOutdent(event.oldRange, event.oldText) 106 | 107 | _isIndentOutdent: (range, text)-> 108 | tabLength = @editor.getTabLength() 109 | diff = range.end.column - range.start.column 110 | true if diff == @editor.getTabLength() and range.start.row == range.end.row and @_checkTextForSpaces(text, tabLength) 111 | 112 | _checkTextForSpaces: (text, tabSize)-> 113 | return false unless text and text.length is tabSize 114 | 115 | for ch in text 116 | return false unless ch is " " 117 | true 118 | 119 | module.exports = Mark 120 | -------------------------------------------------------------------------------- /lib/search-manager.coffee: -------------------------------------------------------------------------------- 1 | {Point, Range} = require 'atom' 2 | Search = require './search' 3 | SearchResults = require './search-results' 4 | SearchView = require './search-view' 5 | Utils = require './utils' 6 | 7 | module.exports = 8 | class SearchManager 9 | constructor: (@atomicEmacs) -> 10 | @emacsEditor = null 11 | 12 | @searchView = null 13 | @originCursors = null 14 | @checkpointCursors = null 15 | 16 | @search = null 17 | @results = null 18 | 19 | destroy: -> 20 | @exit() 21 | @results?.clear() 22 | @search?.stop() 23 | @searchView?.destroy() 24 | 25 | start: (@emacsEditor, {direction}) -> 26 | @searchView ?= new SearchView(this) 27 | @searchView.start({direction}) 28 | @originCursors = @emacsEditor.saveCursors() 29 | @checkpointCursors = @originCursors 30 | 31 | exit: -> 32 | if @searchView? 33 | @searchView.exit() 34 | @emacsEditor.editor.element.focus() 35 | 36 | cancel: -> 37 | @searchView.cancel() 38 | @emacsEditor.editor.element.focus() 39 | 40 | repeat: (direction) -> 41 | if @searchView.isEmpty() 42 | @searchView.repeatLastQuery(direction) 43 | return 44 | 45 | if @results? 46 | @checkpointCursors = @emacsEditor.saveCursors() 47 | @_advanceCursors(direction) 48 | 49 | toggleCaseSensitivity: -> 50 | @searchView.toggleCaseSensitivity() 51 | 52 | toggleIsRegExp: -> 53 | @searchView.toggleIsRegExp() 54 | 55 | yankWordOrCharacter: -> 56 | if @emacsEditor.editor.hasMultipleCursors() 57 | atom.notifications.addInfo "Can't yank into search when using multiple cursors" 58 | 59 | emacsCursor = @emacsEditor.getEmacsCursors()[0] 60 | range = @_wordOrCharacterRangeFrom(emacsCursor) 61 | text = @emacsEditor.editor.getTextInBufferRange(range) 62 | @searchView.append(text) 63 | 64 | isRunning: -> 65 | @search?.isRunning() 66 | 67 | _wordOrCharacterRangeFrom: (emacsCursor) -> 68 | eob = @emacsEditor.editor.getBuffer().getEndPosition() 69 | point = emacsCursor.cursor.getBufferPosition() 70 | alphanumPattern = /[a-z0-9]/i 71 | 72 | nextChar = @emacsEditor.characterAfter(point) 73 | doWord = alphanumPattern.test(nextChar) or 74 | alphanumPattern.test(@emacsEditor.characterAfter(@emacsEditor.positionAfter(point))) 75 | 76 | target = 77 | if doWord 78 | range = emacsCursor.locateForward(alphanumPattern) 79 | if range 80 | range = @emacsEditor.locateForwardFrom(range.start, /[^a-z0-9]/i) 81 | if range then range.start else eob 82 | else 83 | if /[ \t]/.test(nextChar) 84 | range = emacsCursor.locateForward(/[^ \t]/) 85 | if range then range.start else eob 86 | else 87 | @emacsEditor.positionAfter(point) or eob 88 | new Range(point, target) 89 | 90 | changed: (text, {caseSensitive, isRegExp, direction}) -> 91 | @results?.clear() 92 | @search?.stop() 93 | 94 | @results = SearchResults.for(@emacsEditor) 95 | @results.clear() 96 | @searchView.resetProgress() 97 | 98 | return if text == '' 99 | 100 | caseSensitive = caseSensitive or (not isRegExp and /[A-Z]/.test(text)) 101 | 102 | sortedCursors = @checkpointCursors.sort (a, b) -> 103 | headComparison = a.head.compare(b.head) 104 | 105 | wrapped = false 106 | moved = false 107 | canMove = => 108 | if direction == 'forward' 109 | lastCursorPosition = sortedCursors[sortedCursors.length - 1].head 110 | @results.findResultAfter(lastCursorPosition) 111 | else 112 | firstCursorPosition = sortedCursors[0].head 113 | @results.findResultBefore(firstCursorPosition) 114 | 115 | try 116 | regExp = new RegExp( 117 | if isRegExp then text else Utils.escapeForRegExp(text) 118 | if caseSensitive then 'g' else 'ig' 119 | ) 120 | catch e 121 | @searchView.setError(e) 122 | return 123 | 124 | @search = new Search 125 | emacsEditor: @emacsEditor 126 | startPosition: 127 | if direction == 'forward' 128 | sortedCursors[0].head 129 | else 130 | sortedCursors[sortedCursors.length - 1].head 131 | direction: direction 132 | regExp: regExp 133 | onMatch: (range) => 134 | return if not @results? 135 | @results.add(range, wrapped) 136 | if not moved and (canMove() or wrapped) 137 | @emacsEditor.restoreCursors(@checkpointCursors) 138 | @_advanceCursors(direction) 139 | moved = true 140 | onWrapped: -> 141 | wrapped = true 142 | onBlockFinished: => 143 | @_updateSearchView() 144 | onFinished: => 145 | return if not @results? 146 | if not moved 147 | @emacsEditor.restoreCursors(@checkpointCursors) 148 | if @results.numMatches() > 0 149 | @_advanceCursors(direction) 150 | moved = true 151 | @searchView.scanningDone() 152 | 153 | @search?.start() 154 | 155 | _updateSearchView: -> 156 | point = @emacsEditor.editor.getCursors()[0].getBufferPosition() 157 | @searchView.setProgress(@results.numMatchesBefore(point), @results.numMatches()) 158 | 159 | _advanceCursors: (direction) -> 160 | return if not @results? 161 | return if @results.numMatches() == 0 162 | 163 | markers = [] 164 | if direction == 'forward' 165 | @emacsEditor.moveEmacsCursors (emacsCursor) => 166 | marker = @results.findResultAfter(emacsCursor.cursor.getBufferPosition()) 167 | if marker == null 168 | @searchView.showWrapIcon(direction) 169 | marker = @results.findResultAfter(new Point(0, 0)) 170 | emacsCursor.cursor.setBufferPosition(marker.getEndBufferPosition()) 171 | markers.push(marker) 172 | else 173 | @emacsEditor.moveEmacsCursors (emacsCursor) => 174 | marker = @results.findResultBefore(emacsCursor.cursor.getBufferPosition()) 175 | if marker == null 176 | @searchView.showWrapIcon(direction) 177 | marker = @results.findResultBefore(@emacsEditor.editor.getBuffer().getEndPosition()) 178 | emacsCursor.cursor.setBufferPosition(marker.getStartBufferPosition()) 179 | markers.push(marker) 180 | 181 | pos = @emacsEditor.editor.getCursors()[0].getBufferPosition() 182 | @emacsEditor.editor.scrollToBufferPosition(pos, center: true) 183 | @results.setCurrent(markers) 184 | @_updateSearchView() 185 | 186 | exited: -> 187 | @_deactivate() 188 | 189 | canceled: -> 190 | @emacsEditor.restoreCursors(@originCursors) 191 | @_deactivate() 192 | 193 | _deactivate: -> 194 | @search?.stop() 195 | @search = null 196 | @results?.clear() 197 | @results = null 198 | @originCursors = null 199 | -------------------------------------------------------------------------------- /lib/search-results.coffee: -------------------------------------------------------------------------------- 1 | {Point, Range} = require 'atom' 2 | Utils = require './utils' 3 | 4 | module.exports = 5 | class SearchResults 6 | @for: (emacsEditor) -> 7 | emacsEditor._atomicEmacsSearchResults ?= new SearchResults(emacsEditor) 8 | 9 | constructor: (@emacsEditor) -> 10 | @editor = @emacsEditor.editor 11 | @markerLayer = @editor.addMarkerLayer() 12 | @editor.decorateMarkerLayer @markerLayer, 13 | type: 'highlight' 14 | class: 'atomic-emacs-search-result' 15 | @_numMatches = 0 16 | @currentDecorations = [] 17 | 18 | clear: -> 19 | @_clearDecorations() 20 | @markerLayer.clear() 21 | @_numMatches = 0 22 | 23 | add: (range) -> 24 | @_numMatches += 1 25 | @markerLayer.bufferMarkerLayer.markRange(range) 26 | 27 | numMatches: -> 28 | @_numMatches 29 | 30 | numMatchesBefore: (point) -> 31 | markers = @markerLayer.findMarkers 32 | startsInRange: new Range(new Point(0, 0), point) 33 | markers.length 34 | 35 | findResultAfter: (point) -> 36 | markers = @markerLayer.findMarkers 37 | startsInRange: new Range(point, @editor.getBuffer().getEndPosition()) 38 | markers[0] or null 39 | 40 | findResultBefore: (point) -> 41 | if point.isEqual(Utils.BOB) 42 | return null 43 | 44 | markers = @markerLayer.findMarkers 45 | startsInRange: new Range(new Point(0, 0), @emacsEditor.positionBefore(point)) 46 | markers[markers.length - 1] or null 47 | 48 | setCurrent: (markers) -> 49 | @_clearDecorations() 50 | 51 | @currentDecorations = markers.map (marker) => 52 | @editor.decorateMarker marker, 53 | type: 'highlight' 54 | class: 'atomic-emacs-current-result' 55 | 56 | getCurrent: -> 57 | @currentDecorations.map (d) -> d.getMarker() 58 | 59 | _clearDecorations: -> 60 | @currentDecorations.forEach (decoration) -> 61 | decoration.destroy() 62 | -------------------------------------------------------------------------------- /lib/search-view.coffee: -------------------------------------------------------------------------------- 1 | {TextEditor} = require 'atom' 2 | 3 | module.exports = 4 | class SearchView 5 | constructor: (@searchManager) -> 6 | @searchEditor = new TextEditor(mini: true) 7 | @searchEditor.element.setAttribute('id', 'atomic_emacs_search_editor') 8 | @searchEditor.onDidChange => @_runQuery() if @active 9 | 10 | @element = document.createElement('div') 11 | @element.classList.add('atomic-emacs', 'search') 12 | @element.innerHTML = """ 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 | 23 | """ 24 | 25 | @label = @element.querySelector('label') 26 | @caseSensitivityButton = @element.querySelector('.case-sensitivity') 27 | @isRegExpButton = @element.querySelector('.is-reg-exp') 28 | @errorView = @element.querySelector('.error') 29 | 30 | placeholder = @element.querySelector('.SEARCH-EDITOR') 31 | placeholder.parentNode.replaceChild(@searchEditor.element, placeholder) 32 | 33 | @wrapIcon = document.createElement('div') 34 | @wrapIcon.classList.add('atomic-emacs', 'search-wrap-icon') 35 | 36 | @caseSensitive = false 37 | @_updateCaseSensitivityButton() 38 | @caseSensitivityButton.addEventListener 'click', (event) => 39 | @toggleCaseSensitivity() 40 | 41 | @isRegExp = false 42 | @_updateIsRegExpButton() 43 | @isRegExpButton.addEventListener 'click', (event) => 44 | @toggleIsRegExp() 45 | 46 | @panel = atom.workspace.addBottomPanel 47 | item: this 48 | visible: false 49 | 50 | @active = false 51 | @lastQuery = null 52 | 53 | @workspaceFocusInListener = (event) => 54 | if @active and event.target.closest('.atomic-emacs.search') == null 55 | @exit() 56 | 57 | destroy: -> 58 | @searchEditor.destroy() 59 | @panel.destroy() 60 | 61 | start: ({@direction}) -> 62 | @_activate() 63 | @searchEditor.element.focus() 64 | 65 | exit: -> 66 | @_deactivate() 67 | @searchManager.exited() 68 | 69 | cancel: -> 70 | @_deactivate() 71 | @searchManager.canceled() 72 | 73 | isEmpty: -> 74 | @searchEditor.isEmpty() 75 | 76 | repeatLastQuery: (@direction) -> 77 | if @lastQuery 78 | @searchEditor.setText(@lastQuery) 79 | 80 | toggleCaseSensitivity: -> 81 | @caseSensitive = not @caseSensitive 82 | @_updateCaseSensitivityButton() 83 | @_runQuery() if @active 84 | 85 | toggleIsRegExp: -> 86 | @isRegExp = not @isRegExp 87 | @_updateIsRegExpButton() 88 | @_runQuery() if @active 89 | 90 | append: (text) -> 91 | @searchEditor.setText(@searchEditor.getText() + text) 92 | 93 | resetProgress: -> 94 | @total = 0 95 | @isScanning = true 96 | @currentIndex = null 97 | @label.innerText = "Search:" 98 | @errorView.classList.add('hidden') 99 | 100 | setProgress: (currentIndex, total) -> 101 | @currentIndex = currentIndex 102 | @total = total 103 | @_updateLabel() 104 | 105 | setError: (error) -> 106 | @error = error 107 | @errorView.innerText = error.message 108 | @errorView.classList.remove('hidden') 109 | 110 | scanningDone: -> 111 | @isScanning = false 112 | @_updateLabel() 113 | 114 | showWrapIcon: (direction) -> 115 | # Adapted from find-and-replace's FindView#showWrapIcon(). 116 | activePaneItem = atom.workspace.getCenter().getActivePaneItem() 117 | return if not activePaneItem? 118 | 119 | paneItemView = atom.views.getView(activePaneItem) 120 | return if not paneItemView? 121 | 122 | paneItemView.parentNode.appendChild(@wrapIcon) 123 | [icon, otherIcon] = 124 | if direction == 'forward' 125 | ['icon-move-up', 'icon-move-down'] 126 | else 127 | ['icon-move-down', 'icon-move-up'] 128 | @wrapIcon.classList.remove(otherIcon) 129 | @wrapIcon.classList.add(icon, 'visible') 130 | clearTimeout(@showWrapTimeout) 131 | clearTimeout(@hideWrapTimeout) 132 | @showWrapTimeout = setTimeout((=> 133 | @wrapIcon.classList.remove('visible') 134 | @hideWrapTimeout = setTimeout((=> 135 | if paneItemView.parentNode == @wrapIcon.parentNode 136 | paneItemView.parentNode.removeChild(@wrapIcon) 137 | ), 500) 138 | ), 500) 139 | 140 | _activate: -> 141 | @active = true 142 | @resetProgress() 143 | @panel.show() 144 | 145 | workspaceView = atom.views.getView(atom.workspace) 146 | workspaceView.classList.add('atomic-emacs-search-visible') 147 | workspaceView.addEventListener 'focusin', @workspaceFocusInListener 148 | 149 | _deactivate: -> 150 | @active = false 151 | @searchEditor.setText('') 152 | @panel.hide() 153 | workspaceView = atom.views.getView(atom.workspace) 154 | workspaceView.classList.remove('atomic-emacs-search-visible') 155 | workspaceView.removeEventListener 'focusin', @workspaceFocusInListener 156 | 157 | _runQuery: -> 158 | text = @searchEditor.getText() 159 | @lastQuery = text 160 | @searchManager.changed(text, {@caseSensitive, @isRegExp, @direction}) 161 | 162 | _updateLabel: -> 163 | if @total == 0 and not @isScanning 164 | @label.innerHTML = 'No matches' 165 | else 166 | currentIndex = @currentIndex ? '?' 167 | total = @total ? '?' 168 | ellipsis = if @isScanning then '...' else '' 169 | @label.innerHTML = "#{currentIndex} of #{total}#{ellipsis}" 170 | 171 | _updateCaseSensitivityButton: -> 172 | method = if @caseSensitive then 'add' else 'remove' 173 | @caseSensitivityButton.classList[method]('selected') 174 | 175 | _updateIsRegExpButton: -> 176 | method = if @isRegExp then 'add' else 'remove' 177 | @isRegExpButton.classList[method]('selected') 178 | 179 | _caseSensitivitySVG: -> 180 | """ 181 | 182 | 183 | 184 | """ 185 | 186 | _isRegExpSVG: -> 187 | """ 188 | 189 | 190 | 191 | 192 | 193 | 194 | """ 195 | -------------------------------------------------------------------------------- /lib/search.coffee: -------------------------------------------------------------------------------- 1 | {Point, Range} = require 'atom' 2 | Utils = require './utils' 3 | 4 | # Handles the search through the buffer from a given starting point, in a given 5 | # direction, wrapping back around to the starting point. Each call to proceed() 6 | # advances up to a limited distance, calling the onMatch callback at most once, 7 | # and return true until the starting point has been reached again. Once that 8 | # happens, proceed() will return false, and will never call the onMatch callback 9 | # anymore. 10 | module.exports = 11 | class Search 12 | constructor: ({@emacsEditor, @startPosition, @direction, @regExp, @onMatch, @onBlockFinished, @onWrapped, @onFinished, @blockLines}) -> 13 | @editor = @emacsEditor.editor 14 | @blockLines ?= 200 15 | 16 | @_buffer = @editor.getBuffer() 17 | eob = @_buffer.getEndPosition() 18 | [@bufferLimit, @bufferReverseLimit] = 19 | if @direction == 'forward' then [eob, Utils.BOB] else [Utils.BOB, eob] 20 | 21 | # TODO: Don't assume regExp can't span lines. need a configurable overlap? 22 | @_startBlock(@startPosition) 23 | 24 | @_wrapped = false 25 | @_finished = false 26 | @_stopRequested = false 27 | 28 | isRunning: -> 29 | not @_finished 30 | 31 | start: -> 32 | task = => 33 | if not @_stopRequested and @_proceed() 34 | setTimeout(task, 0) 35 | setTimeout(task, 0) 36 | 37 | stop: -> 38 | @_stopRequested = true 39 | 40 | # Proceed with the scan until either a match, or the end of the current range 41 | # is reached. Return true if the search isn't finished yet, false otherwise. 42 | _proceed: -> 43 | return false if @_finished 44 | 45 | found = false 46 | 47 | if @direction == 'forward' 48 | @editor.scanInBufferRange @regExp, new Range(@currentPosition, @currentLimit), ({range}) => 49 | found = true 50 | @onMatch(range) 51 | # If range is empty, advance one char to ensure finite progress. 52 | if range.isEmpty() 53 | @currentPosition = @_buffer.positionForCharacterIndex(@_buffer.characterIndexForPosition(range.end) + 1) 54 | else 55 | @currentPosition = range.end 56 | else 57 | @editor.backwardsScanInBufferRange @regExp, new Range(@currentLimit, @currentPosition), ({range}) => 58 | found = true 59 | @onMatch(range) 60 | # If range is empty, advance one char to ensure finite progress. 61 | if range.isEmpty() 62 | @currentPosition = @_buffer.positionForCharacterIndex(@_buffer.characterIndexForPosition(range.start) - 1) 63 | else 64 | @currentPosition = range.start 65 | @onBlockFinished() 66 | 67 | if @_wrapped and @currentLimit.isEqual(@startPosition) 68 | @_finished = true 69 | @onFinished() 70 | return false 71 | else if not @_wrapped and @currentLimit.isEqual(@bufferLimit) 72 | @_wrapped = true 73 | @onWrapped() 74 | @_startBlock(@bufferReverseLimit) 75 | else 76 | @_startBlock(@currentLimit) 77 | 78 | true 79 | 80 | _startBlock: (blockStart) -> 81 | @currentPosition = blockStart 82 | @currentLimit = @_nextLimit(blockStart) 83 | 84 | _nextLimit: (point) -> 85 | if @direction == 'forward' 86 | guess = new Point(point.row + @blockLines, 0) 87 | limit = if @_wrapped then @startPosition else @bufferLimit 88 | if guess.isGreaterThan(limit) then limit else guess 89 | else 90 | guess = new Point(point.row - @blockLines, 0) 91 | limit = if @_wrapped then @startPosition else @bufferLimit 92 | if guess.isLessThan(limit) then limit else guess 93 | -------------------------------------------------------------------------------- /lib/state.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | initialize: -> 3 | @_killed = @killing = false 4 | @_yanked = @yanking = false 5 | @previousCommand = null 6 | @recenters = 0 7 | @_recentered = false 8 | 9 | beforeCommand: (event) -> 10 | # Some plugins like "intentions" bind things to the pressing of a modifier, 11 | # which should not be able to cancel a dabbrev. 12 | if not @_isModifierKeyEvent(event) and not /dabbrev/.test(event.type) and @dabbrevState? 13 | @dabbrevState.emacsEditor.dabbrevDone() 14 | @dabbrevState = null 15 | @isDuringCommand = true 16 | 17 | afterCommand: (event) -> 18 | if (@killing = @_killed) 19 | @_killed = false 20 | 21 | if (@yanking = @_yanked) 22 | @_yanked = false 23 | 24 | if @_recentered 25 | @_recentered = false 26 | @recenters = (@recenters + 1) % 3 27 | else 28 | @recenters = 0 29 | 30 | @previousCommand = event.type 31 | @isDuringCommand = false 32 | 33 | killed: -> 34 | @_killed = true 35 | 36 | yanked: -> 37 | @_yanked = true 38 | 39 | recentered: -> 40 | @_recentered = true 41 | 42 | yankComplete: -> @yanking and not @_yanked 43 | 44 | _isModifierKeyEvent: (event) -> 45 | event.originalEvent?.constructor is KeyboardEvent and 46 | [0x10, 0x11, 0x12, 0x5b, 0x5d].includes(event.originalEvent.which) 47 | -------------------------------------------------------------------------------- /lib/utils.coffee: -------------------------------------------------------------------------------- 1 | {Point} = require 'atom' 2 | 3 | module.exports = 4 | BOB: new Point(0, 0) 5 | 6 | # Stolen from underscore-plus. 7 | escapeForRegExp: (string) -> 8 | if string 9 | string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') 10 | else 11 | '' 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atomic-emacs", 3 | "main": "./lib/atomic-emacs", 4 | "version": "0.15.0", 5 | "description": "An atomic implementation of emacs keybindings", 6 | "activationCommands": { 7 | "atom-text-editor": [ 8 | "atomic-emacs:backward-char", 9 | "atomic-emacs:forward-char", 10 | "atomic-emacs:backward-word", 11 | "atomic-emacs:forward-word", 12 | "atomic-emacs:backward-sexp", 13 | "atomic-emacs:forward-sexp", 14 | "atomic-emacs:backward-list", 15 | "atomic-emacs:forward-list", 16 | "atomic-emacs:previous-line", 17 | "atomic-emacs:next-line", 18 | "atomic-emacs:backward-paragraph", 19 | "atomic-emacs:forward-paragraph", 20 | "atomic-emacs:back-to-indentation", 21 | "atomic-emacs:backward-kill-word", 22 | "atomic-emacs:kill-word", 23 | "atomic-emacs:kill-line", 24 | "atomic-emacs:kill-region", 25 | "atomic-emacs:copy-region-as-kill", 26 | "atomic-emacs:append-next-kill", 27 | "atomic-emacs:yank", 28 | "atomic-emacs:yank-pop", 29 | "atomic-emacs:yank-shift", 30 | "atomic-emacs:cut", 31 | "atomic-emacs:copy", 32 | "atomic-emacs:delete-horizontal-space", 33 | "atomic-emacs:delete-indentation", 34 | "atomic-emacs:open-line", 35 | "atomic-emacs:just-one-space", 36 | "atomic-emacs:transpose-chars", 37 | "atomic-emacs:transpose-words", 38 | "atomic-emacs:transpose-lines", 39 | "atomic-emacs:downcase-word-or-region", 40 | "atomic-emacs:upcase-word-or-region", 41 | "atomic-emacs:capitalize-word-or-region", 42 | "atomic-emacs:dabbrev-expand", 43 | "atomic-emacs:dabbrev-previous", 44 | "atomic-emacs:isearch-forward", 45 | "atomic-emacs:isearch-backward", 46 | "atomic-emacs:isearch-exit", 47 | "atomic-emacs:isearch-cancel", 48 | "atomic-emacs:isearch-repeat-forward", 49 | "atomic-emacs:isearch-repeat-backward", 50 | "atomic-emacs:isearch-toggle-case-fold", 51 | "atomic-emacs:isearch-toggle-regexp", 52 | "atomic-emacs:isearch-yank-word-or-character", 53 | "atomic-emacs:set-mark", 54 | "atomic-emacs:mark-sexp", 55 | "atomic-emacs:mark-whole-buffer", 56 | "atomic-emacs:exchange-point-and-mark", 57 | "atomic-emacs:recenter-top-bottom", 58 | "atomic-emacs:scroll-down", 59 | "atomic-emacs:scroll-up" 60 | ], 61 | "atom-workspace": [ 62 | "atomic-emacs:find-file", 63 | "atomic-emacs:close-other-panes" 64 | ] 65 | }, 66 | "consumedServices": { 67 | "file-icons.element-icons": { 68 | "versions": { 69 | "1.0.0": "consumeElementIcons" 70 | } 71 | } 72 | }, 73 | "providedServices": { 74 | "atomic-emacs": { 75 | "description": "Atomic Emacs extension interface", 76 | "versions": { 77 | "0.13.0": "service_0_13" 78 | } 79 | } 80 | }, 81 | "repository": "https://github.com/avendael/atomic-emacs", 82 | "license": "MIT", 83 | "engines": { 84 | "atom": ">=1.0.8" 85 | }, 86 | "dependencies": {} 87 | } 88 | -------------------------------------------------------------------------------- /spec/emacs-cursor-spec.coffee: -------------------------------------------------------------------------------- 1 | EmacsEditor = require '../lib/emacs-editor' 2 | EmacsCursor = require '../lib/emacs-cursor' 3 | KillRing = require '../lib/kill-ring' 4 | TestEditor = require './test-editor' 5 | 6 | rangeCoordinates = (range) -> 7 | if range 8 | [range.start.row, range.start.column, range.end.row, range.end.column] 9 | else 10 | range 11 | 12 | describe "EmacsCursor", -> 13 | beforeEach -> 14 | waitsForPromise => 15 | atom.workspace.open().then (editor) => 16 | @editor = editor 17 | @testEditor = new TestEditor(editor) 18 | @emacsEditor = EmacsEditor.for(editor) 19 | @emacsCursor = @emacsEditor.getEmacsCursorFor(editor.getLastCursor()) 20 | 21 | describe "destroy", -> 22 | beforeEach -> 23 | @testEditor.setState("[0].") 24 | @emacsCursor = @emacsEditor.getEmacsCursorFor(@editor.getCursors()[0]) 25 | @startingMarkerCount = @editor.getMarkers().length 26 | 27 | it "cleans up markers set by the mark", -> 28 | @emacsCursor.mark().set().activate() 29 | expect(@editor.getMarkers().length).toBeGreaterThan(@startingMarkerCount) 30 | 31 | @emacsCursor.destroy() 32 | expect(@editor.getMarkers().length).toEqual(@startingMarkerCount) 33 | 34 | it "cleans up the yank marker", -> 35 | @emacsCursor.killRing().push('x') 36 | @emacsCursor.yank() 37 | expect(@editor.getMarkers().length).toBeGreaterThan(@startingMarkerCount) 38 | 39 | @emacsCursor.destroy() 40 | expect(@editor.getMarkers().length).toEqual(@startingMarkerCount) 41 | 42 | describe "mark", -> 43 | it "returns a mark for the cursor", -> 44 | @testEditor.setState("a[0]b[1]c") 45 | [emacsCursor0, emacsCursor1] = @emacsEditor.getEmacsCursors() 46 | expect(emacsCursor0.mark().cursor).toBe(emacsCursor0.cursor) 47 | expect(emacsCursor1.mark().cursor).toBe(emacsCursor1.cursor) 48 | 49 | it "returns the same Mark each time for a cursor", -> 50 | a = @emacsCursor.mark() 51 | b = @emacsCursor.mark() 52 | expect(a).toBe(b) 53 | 54 | describe "killRing", -> 55 | beforeEach -> 56 | @testEditor.setState("[0].") 57 | @emacsCursor = @emacsEditor.getEmacsCursors()[0] 58 | 59 | describe "when the editor has a single cursor", -> 60 | it "returns the global kill ring", -> 61 | expect(@emacsCursor.killRing()).toBe(KillRing.global) 62 | 63 | describe "when the editor has multiple cursors", -> 64 | beforeEach -> 65 | @testEditor.setState("[0].[1]") 66 | [@emacsCursor0, @emacsCursor1] = @emacsEditor.getEmacsCursors() 67 | 68 | it "returns a kill ring for the cursor", -> 69 | killRing0 = @emacsCursor0.killRing() 70 | killRing1 = @emacsCursor1.killRing() 71 | expect(killRing0.constructor).toBe(KillRing) 72 | expect(killRing1.constructor).toBe(KillRing) 73 | expect(killRing0).not.toBe(killRing1) 74 | 75 | it "returns the same KillRing each time for a cursor", -> 76 | a = @emacsCursor0.killRing() 77 | b = @emacsCursor0.killRing() 78 | expect(a).toBe(b) 79 | 80 | describe "locateBackward", -> 81 | it "returns the range of the previous match if found", -> 82 | @testEditor.setState("xx xx [0] xx xx") 83 | range = @emacsCursor.locateBackward(/x+/) 84 | expect(rangeCoordinates(range)).toEqual([0, 3, 0, 5]) 85 | expect(@testEditor.getState()).toEqual("xx xx [0] xx xx") 86 | 87 | it "returns null if no match is found", -> 88 | @testEditor.setState("[0]") 89 | range = @emacsCursor.locateBackward(/x+/) 90 | expect(range).toBe(null) 91 | expect(@testEditor.getState()).toEqual("[0]") 92 | 93 | describe "locateForward", -> 94 | it "returns the range of the next match if found", -> 95 | @testEditor.setState("xx xx [0] xx xx") 96 | range = @emacsCursor.locateForward(/x+/) 97 | expect(rangeCoordinates(range)).toEqual([0, 7, 0, 9]) 98 | expect(@testEditor.getState()).toEqual("xx xx [0] xx xx") 99 | 100 | it "returns null if no match is found", -> 101 | @testEditor.setState("[0]") 102 | range = @emacsCursor.locateForward(/x+/) 103 | expect(range).toBe(null) 104 | expect(@testEditor.getState()).toEqual("[0]") 105 | 106 | describe "locateWordCharacterBackward", -> 107 | it "returns the range of the previous word character if found", -> 108 | @testEditor.setState(" xx [0]") 109 | range = @emacsCursor.locateWordCharacterBackward() 110 | expect(rangeCoordinates(range)).toEqual([0, 2, 0, 3]) 111 | expect(@testEditor.getState()).toEqual(" xx [0]") 112 | 113 | it "returns null if there are no word characters behind", -> 114 | @testEditor.setState(" [0]") 115 | range = @emacsCursor.locateWordCharacterBackward() 116 | expect(range).toBe(null) 117 | expect(@testEditor.getState()).toEqual(" [0]") 118 | 119 | describe "locateWordCharacterForward", -> 120 | it "returns the range of the next word character if found", -> 121 | @testEditor.setState("[0] xx ") 122 | range = @emacsCursor.locateWordCharacterForward() 123 | expect(rangeCoordinates(range)).toEqual([0, 2, 0, 3]) 124 | expect(@testEditor.getState()).toEqual("[0] xx ") 125 | 126 | it "returns null if there are no word characters ahead", -> 127 | @testEditor.setState("[0] ") 128 | range = @emacsCursor.locateWordCharacterForward() 129 | expect(range).toBe(null) 130 | expect(@testEditor.getState()).toEqual("[0] ") 131 | 132 | describe "locateNonWordCharacterBackward", -> 133 | it "returns the range of the previous nonword character if found", -> 134 | @testEditor.setState("x xx[0]") 135 | range = @emacsCursor.locateNonWordCharacterBackward() 136 | expect(rangeCoordinates(range)).toEqual([0, 2, 0, 3]) 137 | expect(@testEditor.getState()).toEqual("x xx[0]") 138 | 139 | it "returns null if there are no nonword characters behind", -> 140 | @testEditor.setState("xx[0]") 141 | range = @emacsCursor.locateNonWordCharacterBackward() 142 | expect(range).toBe(null) 143 | expect(@testEditor.getState()).toEqual("xx[0]") 144 | 145 | describe "locateNonWordCharacterForward", -> 146 | it "returns the range of the next nonword character if found", -> 147 | @testEditor.setState("[0]xx x") 148 | range = @emacsCursor.locateNonWordCharacterForward() 149 | expect(rangeCoordinates(range)).toEqual([0, 2, 0, 3]) 150 | expect(@testEditor.getState()).toEqual("[0]xx x") 151 | 152 | it "returns null if there are no nonword characters ahead", -> 153 | @testEditor.setState("[0]xx") 154 | range = @emacsCursor.locateNonWordCharacterForward() 155 | expect(range).toBe(null) 156 | expect(@testEditor.getState()).toEqual("[0]xx") 157 | 158 | describe "goToMatchStartBackward", -> 159 | it "moves to the start of the previous match and returns true if a match is found", -> 160 | @testEditor.setState("xx xx [0] xx xx") 161 | result = @emacsCursor.goToMatchStartBackward(/x+/) 162 | expect(result).toBe(true) 163 | expect(@testEditor.getState()).toEqual("xx [0]xx xx xx") 164 | 165 | it "does not move and returns false if no match is found", -> 166 | @testEditor.setState("xx xx [0] xx xx") 167 | result = @emacsCursor.goToMatchStartBackward(/y+/) 168 | expect(result).toBe(false) 169 | expect(@testEditor.getState()).toEqual("xx xx [0] xx xx") 170 | 171 | describe "goToMatchStartForward", -> 172 | it "moves to the start of the next match and returns true if a match is found", -> 173 | @testEditor.setState("xx xx [0] xx xx") 174 | result = @emacsCursor.goToMatchStartForward(/x+/) 175 | expect(result).toBe(true) 176 | expect(@testEditor.getState()).toEqual("xx xx [0]xx xx") 177 | 178 | it "does not move and returns false if no match is found", -> 179 | @testEditor.setState("xx xx [0] xx xx") 180 | result = @emacsCursor.goToMatchStartForward(/y+/) 181 | expect(result).toBe(false) 182 | expect(@testEditor.getState()).toEqual("xx xx [0] xx xx") 183 | 184 | describe "goToMatchEndBackward", -> 185 | it "moves to the end of the previous match and returns true if a match is found", -> 186 | @testEditor.setState("xx xx [0] xx xx") 187 | result = @emacsCursor.goToMatchEndBackward(/x+/) 188 | expect(result).toBe(true) 189 | expect(@testEditor.getState()).toEqual("xx xx[0] xx xx") 190 | 191 | it "does not move and returns false if no match is found", -> 192 | @testEditor.setState("xx xx [0] xx xx") 193 | result = @emacsCursor.goToMatchEndBackward(/y+/) 194 | expect(result).toBe(false) 195 | expect(@testEditor.getState()).toEqual("xx xx [0] xx xx") 196 | 197 | describe "goToMatchEndForward", -> 198 | it "moves to the end of the next match and returns true if a match is found", -> 199 | @testEditor.setState("xx xx [0] xx xx") 200 | result = @emacsCursor.goToMatchEndForward(/x+/) 201 | expect(result).toBe(true) 202 | expect(@testEditor.getState()).toEqual("xx xx xx[0] xx") 203 | 204 | it "does not move and returns false if no match is found", -> 205 | @testEditor.setState("xx xx [0] xx xx") 206 | result = @emacsCursor.goToMatchEndForward(/y+/) 207 | expect(result).toBe(false) 208 | expect(@testEditor.getState()).toEqual("xx xx [0] xx xx") 209 | 210 | describe "skipCharactersBackward", -> 211 | it "moves backward over the given characters", -> 212 | @testEditor.setState("x..x..[0]") 213 | @emacsCursor.skipCharactersBackward('.') 214 | expect(@testEditor.getState()).toEqual("x..x[0]..") 215 | 216 | it "does not move if the previous character is not in the list", -> 217 | @testEditor.setState("..x[0]") 218 | @emacsCursor.skipCharactersBackward('.') 219 | expect(@testEditor.getState()).toEqual("..x[0]") 220 | 221 | it "moves to the beginning of the buffer if all prior characters are in the list", -> 222 | @testEditor.setState("..[0]") 223 | @emacsCursor.skipCharactersBackward('.') 224 | expect(@testEditor.getState()).toEqual("[0]..") 225 | 226 | describe "skipCharactersForward", -> 227 | it "moves forward over the given characters", -> 228 | @testEditor.setState("[0]..x..x") 229 | @emacsCursor.skipCharactersForward('.') 230 | expect(@testEditor.getState()).toEqual("..[0]x..x") 231 | 232 | it "does not move if the next character is not in the list", -> 233 | @testEditor.setState("[0]x..") 234 | @emacsCursor.skipCharactersForward('.') 235 | expect(@testEditor.getState()).toEqual("[0]x..") 236 | 237 | it "moves to the end of the buffer if all following characters are in the list", -> 238 | @testEditor.setState("[0]..") 239 | @emacsCursor.skipCharactersForward('.') 240 | expect(@testEditor.getState()).toEqual("..[0]") 241 | 242 | describe "skipWordCharactersBackward", -> 243 | it "moves over any word characters backward", -> 244 | @testEditor.setState("abc abc[0]abc abc") 245 | @emacsCursor.skipWordCharactersBackward() 246 | expect(@testEditor.getState()).toEqual("abc [0]abcabc abc") 247 | 248 | it "does not move if the previous character is not a word character", -> 249 | @testEditor.setState("abc abc [0]") 250 | @emacsCursor.skipWordCharactersBackward() 251 | expect(@testEditor.getState()).toEqual("abc abc [0]") 252 | 253 | it "moves to the beginning of the buffer if all prior characters are word characters", -> 254 | @testEditor.setState("abc[0]") 255 | @emacsCursor.skipWordCharactersBackward() 256 | expect(@testEditor.getState()).toEqual("[0]abc") 257 | 258 | describe "skipWordCharactersForward", -> 259 | it "moves over any word characters forward", -> 260 | @testEditor.setState("abc abc[0]abc abc") 261 | @emacsCursor.skipWordCharactersForward() 262 | expect(@testEditor.getState()).toEqual("abc abcabc[0] abc") 263 | 264 | it "does not move if the next character is not a word character", -> 265 | @testEditor.setState("[0] abc abc") 266 | @emacsCursor.skipWordCharactersForward() 267 | expect(@testEditor.getState()).toEqual("[0] abc abc") 268 | 269 | it "moves to the end of the buffer if all following characters are word characters", -> 270 | @testEditor.setState("[0]abc") 271 | @emacsCursor.skipWordCharactersForward() 272 | expect(@testEditor.getState()).toEqual("abc[0]") 273 | 274 | describe "skipNonWordCharactersBackward", -> 275 | it "moves over any nonword characters backward", -> 276 | @testEditor.setState(" x [0] x ") 277 | @emacsCursor.skipNonWordCharactersBackward() 278 | expect(@testEditor.getState()).toEqual(" x[0] x ") 279 | 280 | it "does not move if the previous character is a word character", -> 281 | @testEditor.setState(" x x[0]") 282 | @emacsCursor.skipNonWordCharactersBackward() 283 | expect(@testEditor.getState()).toEqual(" x x[0]") 284 | 285 | it "moves to the beginning of the buffer if all prior characters are nonword characters", -> 286 | @testEditor.setState(" [0]") 287 | @emacsCursor.skipNonWordCharactersBackward() 288 | expect(@testEditor.getState()).toEqual("[0] ") 289 | 290 | describe "skipNonWordCharactersForward", -> 291 | it "moves over any word characters forward", -> 292 | @testEditor.setState(" x [0] x ") 293 | @emacsCursor.skipNonWordCharactersForward() 294 | expect(@testEditor.getState()).toEqual(" x [0]x ") 295 | 296 | it "does not move if the next character is a word character", -> 297 | @testEditor.setState("[0]x x ") 298 | @emacsCursor.skipNonWordCharactersForward() 299 | expect(@testEditor.getState()).toEqual("[0]x x ") 300 | 301 | it "moves to the end of the buffer if all following characters are nonword characters", -> 302 | @testEditor.setState("[0] ") 303 | @emacsCursor.skipNonWordCharactersForward() 304 | expect(@testEditor.getState()).toEqual(" [0]") 305 | 306 | describe "skipBackwardUntil", -> 307 | it "moves backward over the given characters", -> 308 | @testEditor.setState("x..x..[0]") 309 | @emacsCursor.skipBackwardUntil(/[^\.]/) 310 | expect(@testEditor.getState()).toEqual("x..x[0]..") 311 | 312 | it "does not move if the previous character is not in the list", -> 313 | @testEditor.setState("..x[0]") 314 | @emacsCursor.skipBackwardUntil(/[^\.]/) 315 | expect(@testEditor.getState()).toEqual("..x[0]") 316 | 317 | it "moves to the beginning of the buffer if all prior characters are in the list", -> 318 | @testEditor.setState("..[0]") 319 | @emacsCursor.skipBackwardUntil(/[^\.]/) 320 | expect(@testEditor.getState()).toEqual("[0]..") 321 | 322 | describe "skipForwardUntil", -> 323 | it "moves forward over the given characters", -> 324 | @testEditor.setState("[0]..x..x") 325 | @emacsCursor.skipForwardUntil(/[^\.]/) 326 | expect(@testEditor.getState()).toEqual("..[0]x..x") 327 | 328 | it "does not move if the next character is not in the list", -> 329 | @testEditor.setState("[0]x..") 330 | @emacsCursor.skipForwardUntil(/[^\.]/) 331 | expect(@testEditor.getState()).toEqual("[0]x..") 332 | 333 | it "moves to the end of the buffer if all following characters are in the list", -> 334 | @testEditor.setState("[0]..") 335 | @emacsCursor.skipForwardUntil(/[^\.]/) 336 | expect(@testEditor.getState()).toEqual("..[0]") 337 | 338 | describe "nextCharacter", -> 339 | it "returns the line separator if at the end of a line", -> 340 | @testEditor.setState("ab[0]\ncd") 341 | expect(@emacsCursor.nextCharacter()).toEqual('\n') 342 | 343 | it "return null if at the end of the buffer", -> 344 | @testEditor.setState("ab[0]") 345 | expect(@emacsCursor.nextCharacter()).toBe(null) 346 | 347 | it "returns the character to the right of the cursor otherwise", -> 348 | @testEditor.setState("a[0]b\ncd") 349 | expect(@emacsCursor.nextCharacter()).toEqual('b') 350 | 351 | describe "skipSexpForward", -> 352 | it "skips over the current symbol when inside one", -> 353 | @testEditor.setState("a[0]bc de") 354 | @emacsCursor.skipSexpForward() 355 | expect(@testEditor.getState()).toEqual("abc[0] de") 356 | 357 | it "includes all symbol characters in the symbol", -> 358 | @testEditor.setState("a[0]b_1c de") 359 | @emacsCursor.skipSexpForward() 360 | expect(@testEditor.getState()).toEqual("ab_1c[0] de") 361 | 362 | it "moves over any non-sexp chars before the symbol", -> 363 | @testEditor.setState("[0] .-! ab") 364 | @emacsCursor.skipSexpForward() 365 | expect(@testEditor.getState()).toEqual(" .-! ab[0]") 366 | 367 | it "moves to the end of the buffer if there is nothing after the symbol", -> 368 | @testEditor.setState("a[0]bc") 369 | @emacsCursor.skipSexpForward() 370 | expect(@testEditor.getState()).toEqual("abc[0]") 371 | 372 | it "skips over balanced parentheses if before an open parenthesis", -> 373 | @testEditor.setState("a[0](b)c") 374 | @emacsCursor.skipSexpForward() 375 | expect(@testEditor.getState()).toEqual("a(b)[0]c") 376 | 377 | it "moves over any non-sexp chars before the opening parenthesis", -> 378 | @testEditor.setState("[0] .-! (x)") 379 | @emacsCursor.skipSexpForward() 380 | expect(@testEditor.getState()).toEqual(" .-! (x)[0]") 381 | 382 | it "is not tricked by nested parentheses", -> 383 | @testEditor.setState("a[0]((b c)(\n))d") 384 | @emacsCursor.skipSexpForward() 385 | expect(@testEditor.getState()).toEqual("a((b c)(\n))[0]d") 386 | 387 | it "is not tricked by backslash-escaped parentheses", -> 388 | @testEditor.setState("a[0](b\\)c)d") 389 | @emacsCursor.skipSexpForward() 390 | expect(@testEditor.getState()).toEqual("a(b\\)c)[0]d") 391 | 392 | it "is not tricked by unmatched parentheses", -> 393 | @testEditor.setState("a[0](b]c)d") 394 | @emacsCursor.skipSexpForward() 395 | expect(@testEditor.getState()).toEqual("a(b]c)[0]d") 396 | 397 | it "skips over balanced quotes (assuming it starts outside the quotes)", -> 398 | @testEditor.setState('a[0]"b c"d') 399 | @emacsCursor.skipSexpForward() 400 | expect(@testEditor.getState()).toEqual('a"b c"[0]d') 401 | 402 | it "moves over any non-sexp chars before the opening quote", -> 403 | @testEditor.setState("[0] .-! 'x'") 404 | @emacsCursor.skipSexpForward() 405 | expect(@testEditor.getState()).toEqual(" .-! 'x'[0]") 406 | 407 | it "is not tricked by nested quotes of another type", -> 408 | @testEditor.setState("a[0]'b\"c'd") 409 | @emacsCursor.skipSexpForward() 410 | expect(@testEditor.getState()).toEqual("a'b\"c'[0]d") 411 | 412 | it "does not move if it can't find a matching parenthesis", -> 413 | @testEditor.setState("a[0](b") 414 | @emacsCursor.skipSexpForward() 415 | expect(@testEditor.getState()).toEqual("a[0](b") 416 | 417 | it "does not move if at the end of the buffer", -> 418 | @testEditor.setState("a[0]") 419 | @emacsCursor.skipSexpForward() 420 | expect(@testEditor.getState()).toEqual("a[0]") 421 | 422 | it "does not move if before a closing parenthesis", -> 423 | @testEditor.setState("(a [0]) b") 424 | @emacsCursor.skipSexpForward() 425 | expect(@testEditor.getState()).toEqual("(a [0]) b") 426 | 427 | describe "skipSexpBackward", -> 428 | it "skips over the current symbol when inside one", -> 429 | @testEditor.setState("ab cd[0]e") 430 | @emacsCursor.skipSexpBackward() 431 | expect(@testEditor.getState()).toEqual("ab [0]cde") 432 | 433 | it "includes all symbol characters in the symbol", -> 434 | @testEditor.setState("ab c_1d[0]e") 435 | @emacsCursor.skipSexpBackward() 436 | expect(@testEditor.getState()).toEqual("ab [0]c_1de") 437 | 438 | it "moves over any non-sexp chars after the symbol", -> 439 | @testEditor.setState("ab .-! [0]") 440 | @emacsCursor.skipSexpBackward() 441 | expect(@testEditor.getState()).toEqual("[0]ab .-! ") 442 | 443 | it "moves to the beginning of the buffer if there is nothing before the symbol", -> 444 | @testEditor.setState("ab[0]c") 445 | @emacsCursor.skipSexpBackward() 446 | expect(@testEditor.getState()).toEqual("[0]abc") 447 | 448 | it "skips over balanced parentheses if before an open parenthesis", -> 449 | @testEditor.setState("a(b)[0]c") 450 | @emacsCursor.skipSexpBackward() 451 | expect(@testEditor.getState()).toEqual("a[0](b)c") 452 | 453 | it "moves over any non-sexp chars after the closing parenthesis", -> 454 | @testEditor.setState("(x) .-! [0]") 455 | @emacsCursor.skipSexpBackward() 456 | expect(@testEditor.getState()).toEqual("[0](x) .-! ") 457 | 458 | it "is not tricked by nested parentheses", -> 459 | @testEditor.setState("a((b c)(\n))[0]d") 460 | @emacsCursor.skipSexpBackward() 461 | expect(@testEditor.getState()).toEqual("a[0]((b c)(\n))d") 462 | 463 | it "is not tricked by backslash-escaped parentheses", -> 464 | @testEditor.setState("a(b\\)c)[0]d") 465 | @emacsCursor.skipSexpBackward() 466 | expect(@testEditor.getState()).toEqual("a[0](b\\)c)d") 467 | 468 | it "is not tricked by unmatched parentheses", -> 469 | @testEditor.setState("a(b[c)[0]d") 470 | @emacsCursor.skipSexpBackward() 471 | expect(@testEditor.getState()).toEqual("a[0](b[c)d") 472 | 473 | it "skips over balanced quotes (assuming it starts outside the quotes)", -> 474 | @testEditor.setState('a"b c"[0]d') 475 | @emacsCursor.skipSexpBackward() 476 | expect(@testEditor.getState()).toEqual('a[0]"b c"d') 477 | 478 | it "moves over any non-sexp chars after the closing quote", -> 479 | @testEditor.setState("'x' .-! [0]") 480 | @emacsCursor.skipSexpBackward() 481 | expect(@testEditor.getState()).toEqual("[0]'x' .-! ") 482 | 483 | it "is not tricked by nested quotes of another type", -> 484 | @testEditor.setState("a'b\"c'[0]d") 485 | @emacsCursor.skipSexpBackward() 486 | expect(@testEditor.getState()).toEqual("a[0]'b\"c'd") 487 | 488 | it "does not move if it can't find a matching parenthesis", -> 489 | @testEditor.setState("a)[0]b") 490 | @emacsCursor.skipSexpBackward() 491 | expect(@testEditor.getState()).toEqual("a)[0]b") 492 | 493 | it "does not move if at the beginning of the buffer", -> 494 | @testEditor.setState("[0]a") 495 | @emacsCursor.skipSexpBackward() 496 | expect(@testEditor.getState()).toEqual("[0]a") 497 | 498 | it "does not move if after an opening parenthesis", -> 499 | @testEditor.setState("a ([0] b)") 500 | @emacsCursor.skipSexpBackward() 501 | expect(@testEditor.getState()).toEqual("a ([0] b)") 502 | 503 | describe "skipListForward", -> 504 | it "skips over the next list ahead", -> 505 | @testEditor.setState("a[0]b (c d) e") 506 | @emacsCursor.skipListForward() 507 | expect(@testEditor.getState()).toEqual("ab (c d)[0] e") 508 | 509 | it "does not move if there is no complete list ahead", -> 510 | @testEditor.setState("a[0] (b") 511 | @emacsCursor.skipListForward() 512 | expect(@testEditor.getState()).toEqual("a[0] (b") 513 | 514 | it "does not move if at the end of the buffer", -> 515 | @testEditor.setState("a[0]") 516 | @emacsCursor.skipListForward() 517 | expect(@testEditor.getState()).toEqual("a[0]") 518 | 519 | describe "skipListBackward", -> 520 | it "skips over the previous list", -> 521 | @testEditor.setState("a (b c) d[0]e") 522 | @emacsCursor.skipListBackward() 523 | expect(@testEditor.getState()).toEqual("a [0](b c) de") 524 | 525 | it "does not move if there is no previous complete list", -> 526 | @testEditor.setState("a) [0]b") 527 | @emacsCursor.skipListBackward() 528 | expect(@testEditor.getState()).toEqual("a) [0]b") 529 | 530 | it "does not move if at the beginning of the buffer", -> 531 | @testEditor.setState("[0]a") 532 | @emacsCursor.skipListBackward() 533 | expect(@testEditor.getState()).toEqual("[0]a") 534 | 535 | describe "markSexp", -> 536 | it "selects the next sexp if the selection is not active", -> 537 | @testEditor.setState("a[0] (b c) d") 538 | @emacsCursor.markSexp() 539 | expect(@testEditor.getState()).toEqual("a[0] (b c)(0) d") 540 | 541 | it "extends the selection over the next sexp if the selection is active", -> 542 | @testEditor.setState("a[0] (b c)(0) (d e) f") 543 | @emacsCursor.markSexp() 544 | expect(@testEditor.getState()).toEqual("a[0] (b c) (d e)(0) f") 545 | 546 | it "extends to the end of the buffer if there is no following sexp", -> 547 | @testEditor.setState("a[0] (b c)(0) ") 548 | @emacsCursor.markSexp() 549 | expect(@testEditor.getState()).toEqual("a[0] (b c) (0)") 550 | 551 | it "does nothing if the selection is extended to the end of the buffer", -> 552 | @testEditor.setState("a[0] (b c)(0)") 553 | @emacsCursor.markSexp() 554 | expect(@testEditor.getState()).toEqual("a[0] (b c)(0)") 555 | -------------------------------------------------------------------------------- /spec/emacs-editor-spec.coffee: -------------------------------------------------------------------------------- 1 | {Point} = require 'atom' 2 | EmacsEditor = require '../lib/emacs-editor' 3 | TestEditor = require './test-editor' 4 | 5 | rangeCoordinates = (range) -> 6 | if range 7 | [range.start.row, range.start.column, range.end.row, range.end.column] 8 | else 9 | range 10 | 11 | describe "EmacsEditor", -> 12 | beforeEach -> 13 | waitsForPromise => 14 | atom.workspace.open().then (editor) => 15 | @editor = editor 16 | @testEditor = new TestEditor(editor) 17 | @emacsEditor = EmacsEditor.for(editor) 18 | 19 | describe "saveCursors", -> 20 | it "returns each cursor's head and tail", -> 21 | @testEditor.setState("a[0]b(0)c\nd(1)e[1]f") 22 | result = @emacsEditor.saveCursors() 23 | expect(result.length).toEqual(2) 24 | expect(result[0].head.isEqual(new Point(0, 1))).toBe(true) 25 | expect(result[0].tail.isEqual(new Point(0, 2))).toBe(true) 26 | expect(result[1].head.isEqual(new Point(1, 2))).toBe(true) 27 | expect(result[1].tail.isEqual(new Point(1, 1))).toBe(true) 28 | 29 | it "returns whether each cursor's mark was active", -> 30 | @testEditor.setState("a[0]b(0)c\nd(1)e[1]f") 31 | @emacsEditor.getEmacsCursors().map (c) -> c.mark().set().activate() 32 | result = @emacsEditor.saveCursors() 33 | expect(result.map (c) -> c.markActive).toEqual([true, true]) 34 | 35 | describe "restoreCursors", -> 36 | it "restores state saved by saveCursors", -> 37 | @testEditor.setState("a[0]b(0)c\nd(1)e[1]f") 38 | cursors = @emacsEditor.saveCursors() 39 | 40 | @testEditor.setState('[0]abc\ndef') 41 | @emacsEditor.restoreCursors(cursors) 42 | expect(@testEditor.getState()).toEqual("a[0]b(0)c\nd(1)e[1]f") 43 | 44 | describe "positionAfter", -> 45 | beforeEach -> 46 | @testEditor.setState("abc\ndef") 47 | 48 | it "returns the position to the right if there is one", -> 49 | result = @emacsEditor.positionAfter(new Point(0, 1)) 50 | expect(result.isEqual(new Point(0, 2))).toBe(true) 51 | 52 | it "returns end of line if before the last character in the line", -> 53 | result = @emacsEditor.positionAfter(new Point(0, 2)) 54 | expect(result.isEqual(new Point(0, 3))).toBe(true) 55 | 56 | it "returns the first position on the next line if at end of line", -> 57 | result = @emacsEditor.positionAfter(new Point(0, 3)) 58 | expect(result.isEqual(new Point(1, 0))).toBe(true) 59 | 60 | it "returns null if at end of buffer", -> 61 | result = @emacsEditor.positionAfter(new Point(1, 3)) 62 | expect(result).toBe(null) 63 | 64 | describe "positionBefore", -> 65 | beforeEach -> 66 | @testEditor.setState("abc\ndef") 67 | 68 | it "returns the position tot he left if there is one", -> 69 | result = @emacsEditor.positionBefore(new Point(1, 2)) 70 | expect(result.isEqual(new Point(1, 1))).toBe(true) 71 | 72 | it "returns beginning of line if after the first character in the line", -> 73 | result = @emacsEditor.positionBefore(new Point(1, 1)) 74 | expect(result.isEqual(new Point(1, 0))).toBe(true) 75 | 76 | it "returns the last position on the previous line if at beginning of line", -> 77 | result = @emacsEditor.positionBefore(new Point(1, 0)) 78 | expect(result.isEqual(new Point(0, 3))).toBe(true) 79 | 80 | it "returns null if at beginning of buffer", -> 81 | result = @emacsEditor.positionBefore(new Point(0, 0)) 82 | expect(result).toBe(null) 83 | 84 | describe "characterAfter", -> 85 | beforeEach -> 86 | @testEditor.setState("abc\ndef") 87 | 88 | it "returns the character to the right if there is one", -> 89 | result = @emacsEditor.characterAfter(new Point(0, 1)) 90 | expect(result).toEqual('b') 91 | 92 | it "returns the last character if before the last character in the line", -> 93 | result = @emacsEditor.characterAfter(new Point(0, 2)) 94 | expect(result).toEqual('c') 95 | 96 | it "returns a newline if at end of line", -> 97 | result = @emacsEditor.characterAfter(new Point(0, 3)) 98 | expect(result).toEqual('\n') 99 | 100 | it "returns the first character if at beginning of line", -> 101 | result = @emacsEditor.characterAfter(new Point(1, 0)) 102 | expect(result).toEqual('d') 103 | 104 | it "returns null if at end of buffer", -> 105 | result = @emacsEditor.characterAfter(new Point(1, 3)) 106 | expect(result).toBe(null) 107 | 108 | describe "characterBefore", -> 109 | beforeEach -> 110 | @testEditor.setState("abc\ndef") 111 | 112 | it "returns the character to the left if there is one", -> 113 | result = @emacsEditor.characterBefore(new Point(1, 2)) 114 | expect(result).toEqual('e') 115 | 116 | it "returns the first character if after the first character in the line", -> 117 | result = @emacsEditor.characterBefore(new Point(1, 1)) 118 | expect(result).toEqual('d') 119 | 120 | it "returns a newline if at beginning of line", -> 121 | result = @emacsEditor.characterBefore(new Point(1, 0)) 122 | expect(result).toEqual('\n') 123 | 124 | it "returns the last character if at end of line", -> 125 | result = @emacsEditor.characterBefore(new Point(0, 3)) 126 | expect(result).toEqual('c') 127 | 128 | it "returns null if at beginning of buffer", -> 129 | result = @emacsEditor.characterBefore(new Point(0, 0)) 130 | expect(result).toBe(null) 131 | 132 | describe "locateBackwardFrom", -> 133 | it "returns the range of the previous match from the given point", -> 134 | @testEditor.setState("abcde\nfghij") 135 | range = @emacsEditor.locateBackwardFrom(new Point(1, 4), /b.d/) 136 | expect(rangeCoordinates(range)).toEqual([0, 1, 0, 4]) 137 | 138 | it "returns null if there is no such match", -> 139 | @testEditor.setState("abcde\nfghij") 140 | range = @emacsEditor.locateBackwardFrom(new Point(0, 3), /b.d/) 141 | expect(range).toBe(null) 142 | 143 | describe "locateForwardFrom", -> 144 | it "returns the range of the next match from the given point", -> 145 | @testEditor.setState("abcde\nfghij") 146 | range = @emacsEditor.locateForwardFrom(new Point(0, 1), /g.i/) 147 | expect(rangeCoordinates(range)).toEqual([1, 1, 1, 4]) 148 | 149 | it "returns null if there is no such match", -> 150 | @testEditor.setState("abcde\nfghij") 151 | range = @emacsEditor.locateForwardFrom(new Point(1, 2), /b.d/) 152 | expect(range).toBe(null) 153 | -------------------------------------------------------------------------------- /spec/kill-ring-spec.coffee: -------------------------------------------------------------------------------- 1 | KillRing = require './../lib/kill-ring' 2 | TestEditor = require './test-editor' 3 | 4 | describe "KillRing", -> 5 | beforeEach -> 6 | waitsForPromise => 7 | atom.workspace.open().then (editor) => 8 | @editor = editor 9 | @cursor = @editor.getLastCursor() 10 | @killRing = new KillRing 11 | 12 | describe "constructor", -> 13 | it "creates an empty kill ring", -> 14 | expect(@killRing.getEntries()).toEqual([]) 15 | 16 | describe "fork", -> 17 | it "creates a copy of the kill ring, with the same current entry", -> 18 | @killRing.setEntries(['x', 'y']).rotate(-1) 19 | fork = @killRing.fork() 20 | expect(fork.getEntries()).toEqual(['x', 'y']) 21 | expect(fork.getCurrentEntry()).toEqual('x') 22 | 23 | it "maintains separate state to the original", -> 24 | @killRing.setEntries(['x', 'y']).rotate(-1) 25 | fork = @killRing.fork() 26 | 27 | fork.rotate(1) 28 | expect(fork.getCurrentEntry()).toEqual('y') 29 | 30 | fork.push('z') 31 | expect(fork.getEntries()).toEqual(['x', 'y', 'z']) 32 | 33 | describe "push", -> 34 | it "appends the given entry to the list", -> 35 | @killRing.push('a') 36 | @killRing.push('b') 37 | expect(@killRing.getEntries()).toEqual(['a', 'b']) 38 | 39 | describe "append", -> 40 | it "creates an entry if the kill ring is empty", -> 41 | @killRing.append('a') 42 | expect(@killRing.getEntries()).toEqual(['a']) 43 | 44 | it "appends the given text to the last entry otherwise", -> 45 | @killRing.push('a') 46 | @killRing.push('b') 47 | @killRing.append('c') 48 | expect(@killRing.getEntries()).toEqual(['a', 'bc']) 49 | 50 | describe "prepend", -> 51 | it "creates an entry if the kill ring is empty", -> 52 | @killRing.prepend('a') 53 | expect(@killRing.getEntries()).toEqual(['a']) 54 | 55 | it "prepends the given text to the last entry otherwise", -> 56 | @killRing.push('a') 57 | @killRing.push('b') 58 | @killRing.prepend('c') 59 | expect(@killRing.getEntries()).toEqual(['a', 'cb']) 60 | 61 | describe "rotate", -> 62 | it "rotates the killRing contents", -> 63 | @killRing.push('a') 64 | @killRing.push('b') 65 | @killRing.push('c') 66 | expect(@killRing.getCurrentEntry()).toEqual('c') 67 | expect(@killRing.rotate(-1)).toEqual('b') 68 | expect(@killRing.getCurrentEntry()).toEqual('b') 69 | expect(@killRing.rotate(-1)).toEqual('a') 70 | expect(@killRing.rotate(-1)).toEqual('c') 71 | expect(@killRing.rotate(1)).toEqual('a') 72 | @killRing.push('d') 73 | expect(@killRing.getCurrentEntry()).toEqual('d') 74 | expect(@killRing.rotate(-1)).toEqual('c') 75 | 76 | describe ".pullFromClipboard", -> 77 | beforeEach -> 78 | KillRing.global.reset() 79 | 80 | describe "when a single kill ring (the global one) is given", -> 81 | beforeEach -> 82 | atom.clipboard.write('old') 83 | KillRing.lastClip = 'old' 84 | @killRings = [KillRing.global] 85 | 86 | describe "when there is something new on the clipboard", -> 87 | beforeEach -> 88 | atom.clipboard.write('new') 89 | 90 | it "adds it to the kill ring and updates the last clip", -> 91 | KillRing.pullFromClipboard(@killRings) 92 | expect(@killRings[0].getEntries()).toEqual(['new']) 93 | expect(KillRing.lastClip).toEqual('new') 94 | 95 | describe "when there is nothing new on the clipboard", -> 96 | it "does not update the kill ring", -> 97 | KillRing.pullFromClipboard(@killRings) 98 | expect(@killRings[0].getEntries()).toEqual([]) 99 | 100 | describe "when multiple kill rings are given", -> 101 | beforeEach -> 102 | atom.clipboard.write('old0\nold1\n') 103 | @killRings = [@killRing, new KillRing] 104 | KillRing.lastClip = 'old0\nold1\n' 105 | 106 | describe "when there is something new on the clipboard", -> 107 | it "adds each line to a separate kill ring and updates the last clip", -> 108 | atom.clipboard.write('new0\nnew1') 109 | KillRing.pullFromClipboard(@killRings) 110 | expect(@killRings[0].getEntries()).toEqual(['new0']) 111 | expect(@killRings[1].getEntries()).toEqual(['new1']) 112 | expect(KillRing.lastClip).toEqual('new0\nnew1') 113 | 114 | it "ignores extra lines if there are more lines than kill rings", -> 115 | atom.clipboard.write('new0\nnew1\nnew2') 116 | KillRing.pullFromClipboard(@killRings) 117 | expect(@killRings[0].getEntries()).toEqual(['new0']) 118 | expect(@killRings[1].getEntries()).toEqual(['new1']) 119 | 120 | it "adds entries to all kill rings if there are more kill rings than lines", -> 121 | atom.clipboard.write('new0') 122 | KillRing.pullFromClipboard(@killRings) 123 | expect(@killRings[0].getEntries()).toEqual(['new0']) 124 | expect(@killRings[1].getEntries()).toEqual(['']) 125 | 126 | describe "when there is nothing new on the clipboard", -> 127 | it "does not update the kill rings", -> 128 | KillRing.pullFromClipboard(@killRings) 129 | expect(@killRings[0].getEntries()).toEqual([]) 130 | expect(@killRings[1].getEntries()).toEqual([]) 131 | 132 | describe ".pushToClipboard", -> 133 | beforeEach -> 134 | KillRing.global.reset() 135 | 136 | describe "when a single kill ring (the global one) is given", -> 137 | beforeEach -> 138 | @killRings = [KillRing.global] 139 | atom.clipboard.write('old') 140 | KillRing.lastClip = 'old' 141 | @killRings[0].push('new') 142 | 143 | it "pushes the global kill ring entry to the clipboard", -> 144 | KillRing.pushToClipboard(@killRings) 145 | expect(atom.clipboard.read()).toEqual('new') 146 | 147 | it "updates the last clip, so subsequent pulls don't append again", -> 148 | KillRing.pushToClipboard(@killRings) 149 | expect(KillRing.lastClip).toEqual('new') 150 | 151 | KillRing.pullFromClipboard(@killRings) 152 | expect(KillRing.global.getEntries()).toEqual(['new']) 153 | 154 | describe "when multiple kill rings are given", -> 155 | beforeEach -> 156 | atom.clipboard.write('old0\nold1\n') 157 | @killRings = [@killRing, new KillRing] 158 | KillRing.lastClip = 'old0\nold1\n' 159 | KillRing.global.push('new0\nnew1\n') 160 | 161 | it "pushes the global kill ring entry to the clipboard", -> 162 | KillRing.pushToClipboard(@killRings) 163 | expect(atom.clipboard.read()).toEqual('new0\nnew1\n') 164 | 165 | it "updates the last clip, so subsequent pulls don't append the same thing", -> 166 | KillRing.pushToClipboard(@killRings) 167 | expect(KillRing.lastClip).toEqual('new0\nnew1\n') 168 | 169 | KillRing.pullFromClipboard(@killRings) 170 | expect(@killRings[0].getEntries()).toEqual([]) 171 | expect(@killRings[1].getEntries()).toEqual([]) 172 | expect(KillRing.global.getEntries()).toEqual(['new0\nnew1\n']) 173 | -------------------------------------------------------------------------------- /spec/mark-spec.coffee: -------------------------------------------------------------------------------- 1 | Mark = require './../lib/mark' 2 | TestEditor = require './test-editor' 3 | 4 | describe "Mark", -> 5 | beforeEach -> 6 | waitsForPromise => 7 | atom.workspace.open().then (editor) => 8 | @editor = editor 9 | @testEditor = new TestEditor(@editor) 10 | @cursor = @editor.getLastCursor() 11 | 12 | describe "constructor", -> 13 | it "sets the mark to where the cursor is", -> 14 | @testEditor.setState(".[0]") 15 | mark = new Mark(@cursor) 16 | {row, column} = mark.getBufferPosition() 17 | expect([row, column]).toEqual([0, 1]) 18 | 19 | describe "set", -> 20 | it "sets the mark position to where the cursor is", -> 21 | @testEditor.setState("[0].") 22 | mark = new Mark(@cursor) 23 | 24 | @cursor.setBufferPosition([0, 1]) 25 | expect(mark.getBufferPosition().column).toEqual(0) 26 | 27 | mark.set() 28 | expect(mark.getBufferPosition().column).toEqual(1) 29 | 30 | it "clears the active selection", -> 31 | @testEditor.setState("a(0)b[0]c") 32 | mark = new Mark(@cursor) 33 | expect(@cursor.selection.getText()).toEqual('b') 34 | 35 | mark.set() 36 | expect(@cursor.selection.getText()).toEqual('') 37 | 38 | it "returns the mark so we can conveniently chain an activate() call", -> 39 | mark = new Mark(@cursor) 40 | expect(mark.set()).toBe(mark) 41 | 42 | describe "activate", -> 43 | it "activates the mark", -> 44 | mark = new Mark(@cursor) 45 | mark.activate() 46 | expect(mark.isActive()).toBe(true) 47 | 48 | it "causes cursor movements to extend the selection", -> 49 | @testEditor.setState(".[0]..") 50 | new Mark(@cursor).activate() 51 | @cursor.setBufferPosition([0, 2]) 52 | expect(@testEditor.getState()).toEqual(".(0).[0].") 53 | 54 | it "causes buffer edits to deactivate the mark after the current command", -> 55 | @testEditor.setState(".[0]..") 56 | mark = new Mark(@cursor) 57 | 58 | mark.set().activate() 59 | @cursor.setBufferPosition([0, 2]) 60 | expect(@testEditor.getState()).toEqual(".(0).[0].") 61 | 62 | @editor.setTextInBufferRange([[0, 0], [0, 1]], 'x') 63 | expect(mark.isActive()).toBe(false) 64 | expect(@testEditor.getState()).toEqual("x.[0].") 65 | expect(@cursor.selection.isEmpty()).toBe(true) 66 | 67 | it "doesn't deactive the mark if changes are indents", -> 68 | @testEditor.setState(".[0]..") 69 | mark = new Mark(@cursor) 70 | 71 | mark.set().activate() 72 | @cursor.setBufferPosition([0, 2]) 73 | expect(@testEditor.getState()).toEqual(".(0).[0].") 74 | 75 | @editor.indentSelectedRows() 76 | expect(mark.isActive()).toBe(true) 77 | expect(@testEditor.getState()).toEqual(" .(0).[0].") 78 | expect(@cursor.selection.isEmpty()).toBe(false) 79 | 80 | describe "deactivate", -> 81 | it "deactivates the mark", -> 82 | mark = new Mark(@cursor) 83 | mark.activate() 84 | expect(mark.isActive()).toBe(true) 85 | mark.deactivate() 86 | expect(mark.isActive()).toBe(false) 87 | 88 | it "clears the selection", -> 89 | @testEditor.setState("[0].") 90 | mark = new Mark(@cursor) 91 | mark.activate() 92 | @cursor.setBufferPosition([0, 1]) 93 | expect(@cursor.selection.isEmpty()).toBe(false) 94 | 95 | mark.deactivate() 96 | expect(@cursor.selection.isEmpty()).toBe(true) 97 | 98 | describe "exchange", -> 99 | it "exchanges the cursor and mark", -> 100 | @testEditor.setState("[0].") 101 | mark = new Mark(@cursor) 102 | @cursor.setBufferPosition([0, 1]) 103 | 104 | mark.exchange() 105 | 106 | point = mark.getBufferPosition() 107 | expect([point.row, point.column]).toEqual([0, 1]) 108 | point = @cursor.getBufferPosition() 109 | expect([point.row, point.column]).toEqual([0, 0]) 110 | 111 | it "activates the mark & selection if it wasn't active", -> 112 | @testEditor.setState("[0].") 113 | mark = new Mark(@cursor) 114 | @cursor.setBufferPosition([0, 1]) 115 | 116 | expect(@testEditor.getState()).toEqual(".[0]") 117 | expect(mark.isActive()).toBe(false) 118 | 119 | mark.exchange() 120 | 121 | expect(@testEditor.getState()).toEqual("[0].(0)") 122 | expect(mark.isActive()).toBe(true) 123 | 124 | it "leaves the mark & selection active if it already was", -> 125 | @testEditor.setState("[0].") 126 | mark = new Mark(@cursor) 127 | mark.activate() 128 | @cursor.setBufferPosition([0, 1]) 129 | 130 | expect(@testEditor.getState()).toEqual("(0).[0]") 131 | expect(mark.isActive()).toBe(true) 132 | 133 | mark.exchange() 134 | 135 | expect(@testEditor.getState()).toEqual("[0].(0)") 136 | expect(mark.isActive()).toBe(true) 137 | -------------------------------------------------------------------------------- /spec/search-results-spec.coffee: -------------------------------------------------------------------------------- 1 | {Point, Range} = require 'atom' 2 | EmacsEditor = require '../lib/emacs-editor' 3 | SearchResults = require '../lib/search-results' 4 | TestEditor = require './test-editor' 5 | 6 | markerCoordinates = (marker) -> 7 | if marker 8 | range = marker.getBufferRange() 9 | [range.start.row, range.start.column, range.end.row, range.end.column] 10 | else 11 | marker 12 | 13 | makeRange = (fromRow, fromColumn, toRow, toColumn) -> 14 | new Range(new Point(fromRow, fromColumn), new Point(toRow, toColumn)) 15 | 16 | describe "SearchResults", -> 17 | beforeEach -> 18 | waitsForPromise => 19 | atom.workspace.open().then (editor) => 20 | @editor = editor 21 | @testEditor = new TestEditor(editor) 22 | @emacsEditor = EmacsEditor.for(editor) 23 | @searchResults = SearchResults.for(@emacsEditor) 24 | 25 | describe "numMatches", -> 26 | beforeEach -> 27 | @testEditor.setState("[0]abcd") 28 | 29 | it "returns the number of matches added", -> 30 | expect(@searchResults.numMatches()).toEqual(0) 31 | @searchResults.add(makeRange(0, 1, 0, 2)) 32 | @searchResults.add(makeRange(0, 2, 0, 3)) 33 | expect(@searchResults.numMatches()).toEqual(2) 34 | 35 | it "is reset when cleared", -> 36 | expect(@searchResults.numMatches()).toEqual(0) 37 | @searchResults.add(makeRange(0, 1, 0, 2)) 38 | expect(@searchResults.numMatches()).toEqual(1) 39 | 40 | @searchResults.clear() 41 | 42 | expect(@searchResults.numMatches()).toEqual(0) 43 | @searchResults.add(makeRange(0, 2, 0, 3)) 44 | expect(@searchResults.numMatches()).toEqual(1) 45 | 46 | describe "numMatchesBefore", -> 47 | it "returns the number of matches before the given point", -> 48 | @testEditor.setState("[0]abcdefgh") 49 | @searchResults.add(makeRange(0, 1, 0, 2)) 50 | @searchResults.add(makeRange(0, 3, 0, 4)) 51 | @searchResults.add(makeRange(0, 4, 0, 5)) 52 | expect(@searchResults.numMatchesBefore(new Point(0, 0))).toEqual(0) 53 | expect(@searchResults.numMatchesBefore(new Point(0, 1))).toEqual(1) 54 | expect(@searchResults.numMatchesBefore(new Point(0, 2))).toEqual(1) 55 | expect(@searchResults.numMatchesBefore(new Point(0, 3))).toEqual(2) 56 | expect(@searchResults.numMatchesBefore(new Point(0, 4))).toEqual(3) 57 | expect(@searchResults.numMatchesBefore(new Point(0, 5))).toEqual(3) 58 | 59 | describe "findResultAfter", -> 60 | it "returns the result after the given point", -> 61 | @testEditor.setState("[0]abcdefgh") 62 | @searchResults.add(makeRange(0, 1, 0, 2)) 63 | @searchResults.add(makeRange(0, 2, 0, 3)) 64 | @searchResults.add(makeRange(0, 4, 0, 5)) 65 | expect(markerCoordinates(@searchResults.findResultAfter(new Point(0, 0)))).toEqual([0, 1, 0, 2]) 66 | expect(markerCoordinates(@searchResults.findResultAfter(new Point(0, 1)))).toEqual([0, 1, 0, 2]) 67 | expect(markerCoordinates(@searchResults.findResultAfter(new Point(0, 2)))).toEqual([0, 2, 0, 3]) 68 | expect(markerCoordinates(@searchResults.findResultAfter(new Point(0, 3)))).toEqual([0, 4, 0, 5]) 69 | expect(markerCoordinates(@searchResults.findResultAfter(new Point(0, 4)))).toEqual([0, 4, 0, 5]) 70 | expect(markerCoordinates(@searchResults.findResultAfter(new Point(0, 5)))).toEqual(null) 71 | 72 | it "returns null if the given point is at the end of the buffer", -> 73 | @testEditor.setState("[0]x") 74 | @searchResults.add(makeRange(0, 0, 0, 1)) 75 | expect(markerCoordinates(@searchResults.findResultAfter(@editor.getEofBufferPosition()))).toEqual(null) 76 | 77 | describe "findResultBefore", -> 78 | it "returns the result before the given point", -> 79 | @testEditor.setState("[0]abcdefgh") 80 | @searchResults.add(makeRange(0, 1, 0, 2)) 81 | @searchResults.add(makeRange(0, 3, 0, 4)) 82 | @searchResults.add(makeRange(0, 4, 0, 5)) 83 | expect(markerCoordinates(@searchResults.findResultBefore(new Point(0, 0)))).toEqual(null) 84 | expect(markerCoordinates(@searchResults.findResultBefore(new Point(0, 1)))).toEqual(null) 85 | expect(markerCoordinates(@searchResults.findResultBefore(new Point(0, 2)))).toEqual([0, 1, 0, 2]) 86 | expect(markerCoordinates(@searchResults.findResultBefore(new Point(0, 3)))).toEqual([0, 1, 0, 2]) 87 | expect(markerCoordinates(@searchResults.findResultBefore(new Point(0, 4)))).toEqual([0, 3, 0, 4]) 88 | expect(markerCoordinates(@searchResults.findResultBefore(new Point(0, 5)))).toEqual([0, 4, 0, 5]) 89 | 90 | it "returns null if the given point is the beginning of the buffer", -> 91 | @testEditor.setState("[0]x") 92 | @searchResults.add(makeRange(0, 0, 0, 1)) 93 | expect(markerCoordinates(@searchResults.findResultBefore(new Point(0, 0)))).toEqual(null) 94 | 95 | describe "setCurrent", -> 96 | it "clears existing current markers and decorates the given markers as current", -> 97 | @testEditor.setState("[0]abcdefgh") 98 | marker1 = @searchResults.add(makeRange(0, 1, 0, 2)) 99 | marker2 = @searchResults.add(makeRange(0, 3, 0, 4)) 100 | marker3 = @searchResults.add(makeRange(0, 4, 0, 5)) 101 | 102 | @searchResults.setCurrent([marker3]) 103 | currentMarkers = @editor.getDecorations(class: 'atomic-emacs-current-result').map (d) -> 104 | d.getMarker().bufferMarker 105 | expect(currentMarkers).toEqual([marker3]) 106 | 107 | @searchResults.setCurrent([marker1, marker2]) 108 | currentMarkers = @editor.getDecorations(class: 'atomic-emacs-current-result').map (d) -> 109 | d.getMarker().bufferMarker 110 | expect(currentMarkers).toEqual([marker1, marker2]) 111 | -------------------------------------------------------------------------------- /spec/search-spec.coffee: -------------------------------------------------------------------------------- 1 | {Point, Range} = require 'atom' 2 | EmacsEditor = require '../lib/emacs-editor' 3 | Search = require '../lib/search' 4 | SearchResults = require '../lib/search-results' 5 | TestEditor = require './test-editor' 6 | 7 | makeRange = (fromRow, fromColumn, toRow, toColumn) -> 8 | new Range(new Point(fromRow, fromColumn), new Point(toRow, toColumn)) 9 | 10 | describe 'Search', -> 11 | beforeEach -> 12 | waitsForPromise => 13 | atom.workspace.open().then (editor) => 14 | @editor = editor 15 | @testEditor = new TestEditor(editor) 16 | @emacsEditor = EmacsEditor.for(editor) 17 | @searchResults = SearchResults.for(@emacsEditor) 18 | 19 | describe 'start', -> 20 | isFinished = (callbacks) -> 21 | callbacks.length > 0 and callbacks[callbacks.length - 1][0] == 'finished' 22 | 23 | it "searches forward from the start position for a forward search", -> 24 | @testEditor.setState("abc x [0]abc\nabcx\nabc \n abcabc") 25 | callbacks = [] 26 | search = new Search 27 | emacsEditor: @emacsEditor 28 | startPosition: new Point(0, 6) 29 | direction: 'forward' 30 | regExp: /abc/g 31 | onMatch: (range) -> callbacks.push(['match', range]) 32 | onWrapped: -> callbacks.push(['wrapped']) 33 | onFinished: -> callbacks.push(['finished']) 34 | onBlockFinished: -> callbacks.push(['block finished']) 35 | blockLines: 2 36 | search.start() 37 | until isFinished(callbacks) 38 | advanceClock(1) 39 | expect(callbacks[0]).toEqual(['match', Range.fromObject([[0, 6], [0, 9]])]) 40 | expect(callbacks[1]).toEqual(['match', Range.fromObject([[1, 0], [1, 3]])]) 41 | expect(callbacks[2]).toEqual(['block finished']) 42 | expect(callbacks[3]).toEqual(['match', Range.fromObject([[2, 0], [2, 3]])]) 43 | expect(callbacks[4]).toEqual(['match', Range.fromObject([[3, 1], [3, 4]])]) 44 | expect(callbacks[5]).toEqual(['match', Range.fromObject([[3, 4], [3, 7]])]) 45 | expect(callbacks[6]).toEqual(['block finished']) 46 | expect(callbacks[7]).toEqual(['wrapped']) 47 | expect(callbacks[8]).toEqual(['match', Range.fromObject([[0, 0], [0, 3]])]) 48 | expect(callbacks[9]).toEqual(['block finished']) 49 | expect(callbacks[10]).toEqual(['finished']) 50 | 51 | it "searches backward from the start position for a backward search", -> 52 | @testEditor.setState("abc x abc\nabcx\nabc[0] \n abcabc") 53 | callbacks = [] 54 | search = new Search 55 | emacsEditor: @emacsEditor 56 | startPosition: new Point(2, 3) 57 | direction: 'backward' 58 | regExp: /abc/g 59 | onMatch: (range) -> callbacks.push(['match', range]) 60 | onWrapped: -> callbacks.push(['wrapped']) 61 | onFinished: -> callbacks.push(['finished']) 62 | onBlockFinished: -> callbacks.push(['block finished']) 63 | blockLines: 2 64 | search.start() 65 | until isFinished(callbacks) 66 | advanceClock(1) 67 | expect(callbacks[0]).toEqual(['match', Range.fromObject([[2, 0], [2, 3]])]) 68 | expect(callbacks[1]).toEqual(['match', Range.fromObject([[1, 0], [1, 3]])]) 69 | expect(callbacks[2]).toEqual(['match', Range.fromObject([[0, 6], [0, 9]])]) 70 | expect(callbacks[3]).toEqual(['match', Range.fromObject([[0, 0], [0, 3]])]) 71 | expect(callbacks[4]).toEqual(['block finished']) 72 | expect(callbacks[5]).toEqual(['wrapped']) 73 | expect(callbacks[6]).toEqual(['match', Range.fromObject([[3, 4], [3, 7]])]) 74 | expect(callbacks[7]).toEqual(['match', Range.fromObject([[3, 1], [3, 4]])]) 75 | expect(callbacks[8]).toEqual(['block finished']) 76 | expect(callbacks[9]).toEqual(['finished']) 77 | 78 | it "does not return overlapping matches", -> 79 | @testEditor.setState("[0]bananana") 80 | callbacks = [] 81 | search = new Search 82 | emacsEditor: @emacsEditor 83 | startPosition: new Point(0, 0) 84 | direction: 'forward' 85 | regExp: /ana/g 86 | onMatch: (range) -> callbacks.push(['match', range]) 87 | onWrapped: -> callbacks.push(['wrapped']) 88 | onFinished: -> callbacks.push(['finished']) 89 | onBlockFinished: -> callbacks.push(['block finished']) 90 | search.start() 91 | until isFinished(callbacks) 92 | advanceClock(1) 93 | expect(callbacks[0]).toEqual(['match', Range.fromObject([[0, 1], [0, 4]])]) 94 | expect(callbacks[1]).toEqual(['match', Range.fromObject([[0, 5], [0, 8]])]) 95 | expect(callbacks[2]).toEqual(['block finished']) 96 | expect(callbacks[3]).toEqual(['wrapped']) 97 | expect(callbacks[4]).toEqual(['block finished']) 98 | expect(callbacks[5]).toEqual(['finished']) 99 | 100 | it "passes through blocks without matches ok", -> 101 | @testEditor.setState("[0]abc\n\n\n\nabc") 102 | callbacks = [] 103 | search = new Search 104 | emacsEditor: @emacsEditor 105 | startPosition: new Point(0, 0) 106 | direction: 'forward' 107 | regExp: /abc/g 108 | onMatch: (range) -> callbacks.push(['match', range]) 109 | onWrapped: -> callbacks.push(['wrapped']) 110 | onFinished: -> callbacks.push(['finished']) 111 | onBlockFinished: -> callbacks.push(['block finished']) 112 | blockLines: 2 113 | search.start() 114 | until isFinished(callbacks) 115 | advanceClock(1) 116 | expect(callbacks[0]).toEqual(['match', Range.fromObject([[0, 0], [0, 3]])]) 117 | expect(callbacks[1]).toEqual(['block finished']) 118 | expect(callbacks[2]).toEqual(['block finished']) 119 | expect(callbacks[3]).toEqual(['match', Range.fromObject([[4, 0], [4, 3]])]) 120 | expect(callbacks[4]).toEqual(['block finished']) 121 | expect(callbacks[5]).toEqual(['wrapped']) 122 | expect(callbacks[6]).toEqual(['block finished']) 123 | expect(callbacks[7]).toEqual(['finished']) 124 | -------------------------------------------------------------------------------- /spec/test-editor-spec.coffee: -------------------------------------------------------------------------------- 1 | TestEditor = require './test-editor' 2 | 3 | describe "TestEditor", -> 4 | cursorPosition = (editor, i) -> 5 | cursor = editor.getCursors()[i] 6 | point = cursor?.getBufferPosition() 7 | [point?.row, point?.column] 8 | 9 | cursorRange = (editor, i) -> 10 | cursor = editor.getCursors()[i] 11 | return null if !cursor? 12 | 13 | head = cursor.marker.getHeadBufferPosition() 14 | tail = cursor.marker.getTailBufferPosition() 15 | [head?.row, head?.column, tail?.row, tail?.column] 16 | 17 | beforeEach -> 18 | waitsForPromise => 19 | atom.workspace.open().then (editor) => 20 | @editor = editor 21 | @testEditor = new TestEditor(@editor) 22 | 23 | describe "set", -> 24 | it "sets the buffer text", -> 25 | @testEditor.setState('hi') 26 | expect(@editor.getText()).toEqual('hi') 27 | 28 | it "sets cursors where specified", -> 29 | @testEditor.setState('[0]a[2]b[1]') 30 | expect(@editor.getText()).toEqual('ab') 31 | 32 | expect(cursorPosition(@editor, 0)).toEqual([0, 0]) 33 | expect(cursorPosition(@editor, 1)).toEqual([0, 2]) 34 | expect(cursorPosition(@editor, 2)).toEqual([0, 1]) 35 | 36 | it "handles missing cursors", -> 37 | expect((=> @testEditor.setState('[0]x[2]'))). 38 | toThrow('missing head of cursor 1') 39 | 40 | it "sets forward & reverse selections if tails are specified", -> 41 | @testEditor.setState('a(0)b[1]c[0]d(1)e') 42 | expect(@editor.getText()).toEqual('abcde') 43 | 44 | expect(cursorRange(@editor, 0)).toEqual([0, 3, 0, 1]) 45 | expect(cursorRange(@editor, 1)).toEqual([0, 2, 0, 4]) 46 | 47 | describe "get", -> 48 | it "correctly positions cursors", -> 49 | @editor.setText('abc') 50 | @editor.getLastCursor().setBufferPosition([0, 2]) 51 | @editor.addCursorAtBufferPosition([0, 1]) 52 | expect(@testEditor.getState()).toEqual('a[1]b[0]c') 53 | 54 | it "correctly positions heads & tails of forward & reverse selections", -> 55 | @editor.setText('abcde') 56 | @editor.getLastCursor().selection.setBufferRange([[0, 1], [0, 3]]) 57 | cursor = @editor.addCursorAtBufferPosition([0, 0]) 58 | cursor.selection.setBufferRange([[0, 2], [0, 4]], reversed: true) 59 | expect(@testEditor.getState()).toEqual('a(0)b[1]c[0]d(1)e') 60 | -------------------------------------------------------------------------------- /spec/test-editor.coffee: -------------------------------------------------------------------------------- 1 | {Point} = require 'atom' 2 | 3 | cmp = (a, b) -> if a < b then -1 else if a > b then 1 else 0 4 | 5 | module.exports = 6 | class TestEditor 7 | 8 | constructor: (@editor) -> 9 | 10 | # Set the state of the editor. 11 | # 12 | # State is set as the text of the editor, with the following indicators 13 | # stripped out: 14 | # 15 | # * [i]: Sets the head of cursor i at this location. 16 | # * (i): Sets the tail of cursor i at this location. 17 | setState: (state) -> 18 | @editor.setText(state) 19 | re = /\[(\d+)\]|\((\d+)\)/g 20 | 21 | descriptors = [] 22 | @editor.scan re, (hit) -> 23 | i = parseInt(hit.match[0].slice(1, 2), 10) 24 | descriptors[i] ?= {} 25 | if hit.match[1]? 26 | descriptors[i].head = hit.range.start 27 | else 28 | descriptors[i].tail = hit.range.start 29 | hit.replace('') 30 | 31 | # addCursorAtBufferPosition gets messed up by active selections -- add all 32 | # cursors before setting tails. 33 | for descriptor, i in descriptors 34 | {head} = descriptor or {} 35 | if not head 36 | throw "missing head of cursor #{i}" 37 | 38 | cursor = @editor.getCursors()[i] 39 | if not cursor 40 | cursor = @editor.addCursorAtBufferPosition(head) 41 | else 42 | cursor.setBufferPosition(head) 43 | 44 | for descriptor, i in descriptors 45 | {head, tail} = descriptor or {} 46 | if tail 47 | cursor = @editor.getCursors()[i] 48 | reversed = Point.min(head, tail) is head 49 | cursor.selection.setBufferRange([head, tail], reversed: reversed) 50 | 51 | # Return the state (in the format described for set()) of the editor. 52 | getState: -> 53 | buffer = @editor.getBuffer() 54 | linesWithEndings = ( 55 | [buffer.lineForRow(i), buffer.lineEndingForRow(i)] \ 56 | for i in [0...buffer.getLineCount()] 57 | ) 58 | 59 | insertions = [] 60 | for cursor, i in @editor.getCursors() 61 | head = cursor.marker.getHeadBufferPosition() 62 | tail = cursor.marker.getTailBufferPosition() 63 | insertions.push([head.row, head.column, "[#{i}]"]) 64 | insertions.push([tail.row, tail.column, "(#{i})"]) if not head.isEqual(tail) 65 | 66 | insertions.sort (a, b) -> cmp(a[0], b[0]) or cmp(a[1], b[1]) 67 | insertions.reverse() 68 | for [row, column, text] in insertions 69 | [line, ending] = linesWithEndings[row] 70 | line = line.slice(0, column) + text + line.slice(column) 71 | linesWithEndings[row] = [line, ending] 72 | 73 | (lineWithEnding.join('') for lineWithEnding in linesWithEndings).join('') 74 | -------------------------------------------------------------------------------- /spec/utils-spec.coffee: -------------------------------------------------------------------------------- 1 | Utils = require '../lib/utils' 2 | 3 | describe 'Utils', -> 4 | describe '.escapeForRegExp', -> 5 | it "escapes regexp metacharacters", -> 6 | result = Utils.escapeForRegExp('-/\\^$*+?.()|[]{}') 7 | expect(result).toEqual('\\-\\/\\\\\\^\\$\\*\\+\\?\\.\\(\\)\\|\\[\\]\\{\\}') 8 | -------------------------------------------------------------------------------- /styles/atomic-emacs.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | @import "syntax-variables"; 3 | 4 | .atomic-emacs { 5 | .search { 6 | padding: 3px; 7 | 8 | .row.inputs { 9 | display: flex; 10 | flex-flow: row nowrap; 11 | align-items: center; 12 | } 13 | 14 | .row.error { 15 | color: red; 16 | 17 | &.hidden { 18 | display: none; 19 | } 20 | } 21 | 22 | label { 23 | flex: 0 0 auto; 24 | margin: 0.5em; 25 | min-width: 4em; 26 | text-align: center; 27 | } 28 | 29 | atom-text-editor { 30 | flex: 1 1; 31 | width: 100%; 32 | } 33 | 34 | // Adapted from find-and-replace's styles. 35 | button > svg.icon { 36 | width: 20px; 37 | height: 16px; 38 | vertical-align: middle; 39 | fill: currentColor; 40 | pointer-events: none; 41 | } 42 | } 43 | 44 | // Adapted from find-and-replace's styles. 45 | .search-wrap-icon { 46 | @wrap-size: @font-size * 10; 47 | 48 | position: absolute; 49 | top: 50% !important; 50 | left: 50% !important; 51 | right: initial !important; 52 | bottom: initial !important; 53 | margin-top: @wrap-size * -0.5; 54 | margin-left: @wrap-size * -0.5; 55 | 56 | &:before { 57 | // Octicons look best in sizes that are multiples of 16px 58 | font-size: @wrap-size - mod(@wrap-size, 16px) - 32px; 59 | line-height: @wrap-size; 60 | width: @wrap-size; 61 | height: @wrap-size; 62 | color: @syntax-text-color; 63 | opacity: .5; 64 | } 65 | 66 | opacity: 0; 67 | transition: opacity 0.5s; 68 | &.visible { opacity: 1; } 69 | } 70 | } 71 | 72 | // Result markers. Adapted from the find-and-replace package. 73 | atom-text-editor { 74 | .atomic-emacs-search-result .region { 75 | background-color: transparent; 76 | border-radius: @component-border-radius; 77 | border: 1px solid @syntax-result-marker-color; 78 | box-sizing: border-box; 79 | z-index: 0; 80 | } 81 | 82 | .atomic-emacs-current-result .region { 83 | background-color: white; 84 | border-radius: @component-border-radius; 85 | border: 1px solid @syntax-result-marker-color-selected; 86 | box-sizing: border-box; 87 | z-index: 0; 88 | } 89 | 90 | .atomic-emacs-search-result, 91 | .atomic-emacs-current-result { 92 | display: none; 93 | } 94 | } 95 | 96 | atom-workspace.atomic-emacs-search-visible { 97 | atom-text-editor { 98 | .atomic-emacs-search-result, 99 | .atomic-emacs-current-result { 100 | display: block; 101 | } 102 | } 103 | } 104 | --------------------------------------------------------------------------------