├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── keymaps └── emacs.cson ├── lib ├── config-util.coffee ├── config.coffee ├── emacs.coffee ├── find-file-view.coffee ├── kill-ring-model.coffee ├── kill-ring.coffee ├── mark.coffee └── switch-buffer-view.coffee ├── menus └── emacs.cson ├── package.json ├── spec ├── config-spec.coffee ├── config-util-spec.coffee ├── find-file-view-spec.coffee ├── kill-ring-model-spec.coffee └── kill-ring-spec.coffee └── stylesheets └── 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' -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 fuqcool 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 | ## Deprecated 2 | 3 | This package is no longer under maintain. **Because I realized that there is nothing in this world can replace Emacs. Happy hacking!** 4 | 5 | Atom emacs-mode [![Build Status](https://travis-ci.org/fuqcool/atom-emacs-mode.svg?branch=master)](https://travis-ci.org/fuqcool/atom-emacs-mode) 6 | ====== 7 | 8 | This is an Emacs extension for Atom. 9 | 10 | ## Install 11 | 12 | You can install it from `Atom -> Preferences -> Settings -> Packages`. To enable emacs-mode automatically on Atom starts, put following code to your init script: 13 | 14 | ```coffeescript 15 | atom.packages.activatePackage 'emacs-mode' 16 | ``` 17 | 18 | ## Features 19 | 20 | - Regular Emacs key binding(see below) 21 | - Kill ring 22 | - Buffer finder (C-x b), file finder(C-x C-f) 23 | - Copy text by mouse selection 24 | - Zen mode(hide tabs, sidebar) 25 | - Marker 26 | - Emacs-style cursor 27 | 28 | ## Keymap 29 | 30 | ```coffeescript 31 | '.editor': 32 | 'ctrl-a': 'editor:move-to-first-character-of-line' 33 | 'ctrl-e': 'editor:move-to-end-of-line' 34 | 'ctrl-backspace': 'editor:backspace-to-beginning-of-word' 35 | 'ctrl-j': 'editor:newline' 36 | 'ctrl-o': 'emacs:open-line' 37 | 'alt-f': 'emacs:forward-word' 38 | 'alt-b': 'emacs:backward-word' 39 | 'ctrl-l': 'emacs:recenter' 40 | 'alt-/': 'autocomplete:toggle' 41 | 'ctrl-s': 'find-and-replace:show' 42 | 'ctrl-@': 'emacs:set-mark' 43 | 'alt-;': 'editor:toggle-line-comments' 44 | 'alt-g g': 'go-to-line:toggle' 45 | 46 | '.editor.emacs-marking': 47 | 'right':'core:select-right' 48 | 'ctrl-f':'core:select-right' 49 | 'left':'core:select-left' 50 | 'ctrl-b':'core:select-left' 51 | 'up':'core:select-up' 52 | 'ctrl-p':'core:select-up' 53 | 'down':'core:select-down' 54 | 'ctrl-n':'core:select-down' 55 | 56 | 'div.editor': 57 | 'ctrl-space': 'emacs:set-mark' 58 | 59 | '.workspace': 60 | # cursor 61 | 'ctrl-p': 'core:move-up' 62 | 'ctrl-n': 'core:move-down' 63 | 'ctrl-b': 'core:move-left' 64 | 'ctrl-f': 'core:move-right' 65 | 'alt-v': 'core:page-up' 66 | 'ctrl-v': 'core:page-down' 67 | 'alt->': 'core:move-to-bottom' 68 | 'alt-<': 'core:move-to-top' 69 | 70 | # text manipulation 71 | 'ctrl-w': 'emacs:kill-region' 72 | 'ctrl-y': 'emacs:yank' 73 | 'alt-y': 'emacs:yank-pop' 74 | 'alt-w': 'emacs:kill-ring-save' 75 | 'ctrl-/': 'core:undo' 76 | 'ctrl-x ctrl-s': 'core:save' 77 | 78 | # selection 79 | 'ctrl-x h': 'core:select-all' 80 | 81 | # buffer 82 | 'ctrl-g': 'core:cancel' 83 | 'ctrl-x ctrl-c': 'window:close' 84 | 'ctrl-x k': 'core:close' 85 | 'ctrl-x b': 'emacs:switch-buffer' 86 | 'ctrl-x ctrl-f': 'emacs:find-file' 87 | ``` 88 | 89 | ## Configuration 90 | Below are the default configurations: 91 | 92 | ```coffeescript 93 | 'emacs-mode': 94 | 'hideTabs': false # hide tabs 95 | 'hideSidebar': false # hide tree view 96 | 'useEmacsCursor': true # use emacs style(fat) cursor 97 | 'useFuzzyBufferFinder': false # use default buffer finder 98 | 'useFuzzyFileFinder': false # use default file finder 99 | ``` 100 | 101 | ## Contribution 102 | Pull requests are very welcomed. The only requirement before sending a pull request is to pass the test cases and test your own code. 103 | -------------------------------------------------------------------------------- /keymaps/emacs.cson: -------------------------------------------------------------------------------- 1 | '.editor': 2 | 'ctrl-a': 'editor:move-to-first-character-of-line' 3 | 'ctrl-e': 'editor:move-to-end-of-line' 4 | 'ctrl-backspace': 'editor:delete-to-beginning-of-word' 5 | 'ctrl-j': 'editor:newline' 6 | 'ctrl-o': 'emacs:open-line' 7 | 'alt-f': 'emacs:forward-word' 8 | 'alt-b': 'emacs:backward-word' 9 | 'ctrl-l': 'emacs:recenter' 10 | 'alt-/': 'autocomplete:toggle' 11 | 'ctrl-s': 'find-and-replace:show' 12 | 'ctrl-@': 'emacs:set-mark' 13 | 'alt-;': 'editor:toggle-line-comments' 14 | 'alt-g g': 'go-to-line:toggle' 15 | 16 | '.editor.emacs-marking': 17 | 'right': 'core:select-right' 18 | 'ctrl-f': 'core:select-right' 19 | 'left': 'core:select-left' 20 | 'ctrl-b': 'core:select-left' 21 | 'up': 'core:select-up' 22 | 'ctrl-p': 'core:select-up' 23 | 'down': 'core:select-down' 24 | 'ctrl-n': 'core:select-down' 25 | 'ctrl-e': 'editor:select-to-end-of-line' 26 | 'ctrl-a': 'editor:select-to-beginning-of-line' 27 | 28 | 'div.editor': 29 | 'ctrl-space': 'emacs:set-mark' 30 | 31 | '.workspace': 32 | 'ctrl-x': 'unset!' # prevent cut on linux && windows 33 | 34 | # cursor 35 | 'ctrl-p': 'core:move-up' 36 | 'ctrl-n': 'core:move-down' 37 | 'ctrl-b': 'core:move-left' 38 | 'ctrl-f': 'core:move-right' 39 | 'alt-v': 'core:page-up' 40 | 'ctrl-v': 'core:page-down' 41 | 'alt->': 'core:move-to-bottom' 42 | 'alt-<': 'core:move-to-top' 43 | 44 | # text manipulation 45 | 'ctrl-w': 'emacs:kill-region' 46 | 'ctrl-y': 'emacs:yank' 47 | 'alt-y': 'emacs:yank-pop' 48 | 'alt-w': 'emacs:kill-ring-save' 49 | 'ctrl-/': 'core:undo' 50 | 'ctrl-x ctrl-s': 'core:save' 51 | 52 | #selection 53 | 'ctrl-x h': 'core:select-all' 54 | 55 | # buffer 56 | 'ctrl-g': 'core:cancel' 57 | 'ctrl-x ctrl-c': 'window:close' 58 | 'ctrl-x k': 'core:close' 59 | 'ctrl-x b': 'emacs:switch-buffer' 60 | 'ctrl-x ctrl-f': 'emacs:find-file' 61 | -------------------------------------------------------------------------------- /lib/config-util.coffee: -------------------------------------------------------------------------------- 1 | watch = (key) -> 2 | return if not key? 3 | 4 | if arguments.length is 2 5 | callback = arguments[1] 6 | defaultValue = null 7 | else 8 | defaultValue = arguments[1] 9 | callback = arguments[2] 10 | 11 | value = atom.config.get key 12 | 13 | # if config does not exists and default value is given, write default value 14 | if (not value?) and defaultValue? 15 | atom.config.set key, defaultValue 16 | 17 | atom.config.observe key, -> 18 | callback?(atom.config.get(key) ? null) 19 | 20 | module.exports = 21 | watch: watch 22 | -------------------------------------------------------------------------------- /lib/config.coffee: -------------------------------------------------------------------------------- 1 | config = require './config-util' 2 | 3 | config.watch 'emacs-mode.hideTabs', false, (value) -> 4 | atom.workspaceView.trigger 'emacs:hide-tabs', value 5 | 6 | config.watch 'emacs-mode.hideSidebar', false, (value) -> 7 | atom.workspaceView.trigger 'emacs:hide-sidebar', value 8 | 9 | config.watch 'emacs-mode.useEmacsCursor', true, (value) -> 10 | atom.workspaceView.trigger 'emacs:use-emacs-cursor', value 11 | 12 | config.watch 'emacs-mode.useFuzzyFileFinder', false, (value) -> 13 | atom.workspaceView.trigger 'emacs:use-fuzzy-file-finder', value 14 | 15 | config.watch 'emacs-mode.useFuzzyBufferFinder', false, (value) -> 16 | atom.workspaceView.trigger 'emacs:use-fuzzy-buffer-finder', value 17 | -------------------------------------------------------------------------------- /lib/emacs.coffee: -------------------------------------------------------------------------------- 1 | {Range, Point} = require 'atom' 2 | SwitchBufferView = require './switch-buffer-view' 3 | FindFileView = require './find-file-view.coffee' 4 | EmacsMark = require './mark' 5 | killRing = require './kill-ring' 6 | 7 | module.exports = 8 | activate: (state) -> 9 | atom.workspaceView.command 'emacs:find-file', => @findFile() 10 | atom.workspaceView.command 'emacs:hide-tabs', (event, value) => @hideTabs value 11 | atom.workspaceView.command 'emacs:hide-sidebar', (event, value) => @hideSidebar value 12 | atom.workspaceView.command 'emacs:use-emacs-cursor', (event, value) => @useEmacsCursor value 13 | atom.workspaceView.command 'emacs:use-fuzzy-file-finder', (event, value) => @useFuzzyFileFinder = value 14 | atom.workspaceView.command 'emacs:use-fuzzy-buffer-finder', (event, value) => @useFuzzyBufferFinder = value 15 | 16 | require './config' 17 | 18 | atom.workspaceView.eachEditorView (editorView) => 19 | new EmacsMark(editorView) 20 | 21 | editorView.command 'emacs:switch-buffer', => @switchBuffer() 22 | editorView.command 'emacs:open-line', => @openLine editorView 23 | editorView.command 'emacs:forward-word', => @forwardWord editorView 24 | editorView.command 'emacs:backward-word', => @backwardWord editorView 25 | editorView.command 'emacs:recenter', => @recenter editorView 26 | editorView.command 'emacs:clear-selection', => @clearSelection editorView 27 | 28 | editorView.on 'core:cancel', => editorView.trigger 'emacs:clear-selection' 29 | 30 | @enableKillRing editorView 31 | 32 | atom.workspaceView.on 'editor:attached', (evt) => 33 | @enableKillRing evt.targetView() 34 | 35 | deactivate: -> 36 | 37 | serialize: -> 38 | 39 | switchBuffer: -> 40 | if @useFuzzyBufferFinder 41 | atom.workspaceView.trigger 'fuzzy-finder:toggle-buffer-finder' 42 | else 43 | new SwitchBufferView() 44 | 45 | findFile: -> 46 | if @useFuzzyFileFinder 47 | atom.workspaceView.trigger 'fuzzy-finder:toggle-file-finder' 48 | else 49 | new FindFileView() 50 | 51 | openLine: (editorView) -> 52 | editor = editorView.getEditor() 53 | pos = editor.getCursorBufferPosition() 54 | editor.insertNewline() 55 | editor.setCursorBufferPosition pos 56 | 57 | clearSelection: (editorView) -> 58 | editor = editorView.getEditor() 59 | sel.clear() for sel in editor.getSelections() 60 | 61 | _getChar: (editor, row, col) -> 62 | editor.getTextInBufferRange(new Range(new Point(row, col), new Point(row, col + 1))) 63 | 64 | forwardWord: (editorView) => 65 | editorView.trigger 'editor:move-to-end-of-word' 66 | 67 | backwardWord: (editorView) => 68 | editorView.trigger 'editor:move-to-beginning-of-word' 69 | 70 | hideTabs: (isHide) -> 71 | (if isHide then pane.find('.tab-bar').hide() else pane.find('.tab-bar').show()) for pane in atom.workspaceView.getPanes() 72 | 73 | hideSidebar: (isHide) -> 74 | panel = atom.workspaceView.parent().find '.tool-panel' 75 | if isHide then panel.hide() else panel.show() 76 | 77 | useEmacsCursor: (useEmacs) -> 78 | atom.workspaceView.eachEditorView (editorView) -> 79 | if useEmacs 80 | editorView.addClass 'emacs-cursor' 81 | else 82 | editorView.removeClass 'emacs-cursor' 83 | 84 | recenter: (editorView) -> 85 | editor = editorView.getEditor() 86 | cursorPos = editor.getCursorScreenPosition() 87 | rows = editor.getRowsPerPage() 88 | 89 | topRow = cursorPos.row - parseInt(rows / 2) 90 | topPos = editor.clipScreenPosition [topRow, 0] 91 | 92 | pix = editorView.pixelPositionForScreenPosition topPos 93 | editorView.scrollTop pix.top 94 | 95 | enableKillRing: (editorView) -> 96 | return if editorView.hasClass 'kill-ring' 97 | 98 | editorView.command 'emacs:yank', -> killRing.yank editorView 99 | editorView.command 'emacs:yank-pop', -> killRing.yankPop editorView 100 | editorView.command 'emacs:kill-region', -> killRing.killRegion editorView 101 | editorView.command 'emacs:kill-ring-save', -> 102 | killRing.killRingSave editorView 103 | editorView.trigger 'emacs:clear-selection' 104 | 105 | editorView.command 'emacs:cancel-yank', -> 106 | killRing.cancelYank() 107 | 108 | editorView.on 'mouseup', -> killRing.killRingSave editorView 109 | editorView.on 'core:cancel', -> 110 | editorView.trigger 'emacs:cancel-yank' 111 | 112 | editorView.addClass 'kill-ring' 113 | -------------------------------------------------------------------------------- /lib/find-file-view.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | {SelectListView} = require 'atom' 4 | 5 | module.exports = 6 | class FileFinderView extends SelectListView 7 | initialize: -> 8 | super 9 | @addClass 'overlay from-top' 10 | 11 | pwd = path.join '~', path.sep 12 | editor = atom.workspace.getActiveEditor() 13 | 14 | if editor 15 | uri = path.dirname editor.getUri() 16 | pwd = uri if uri? and uri isnt '.' 17 | 18 | pwd = @ensureTailSep pwd 19 | pwd = @toHome pwd 20 | 21 | # re-render list whenever buffer is changed 22 | @subscribe @filterEditorView.getEditor().getBuffer(), 'changed', => 23 | @setItems @renderItems() 24 | @populateList() 25 | 26 | # use current directory as default 27 | @filterEditorView.setText pwd 28 | atom.workspaceView.appendToBottom this 29 | 30 | @focusFilterEditor() 31 | @disableTab() 32 | 33 | viewForItem: (item) -> 34 | try 35 | uri = @resolveHome item.uri 36 | stat = fs.statSync uri 37 | 38 | if stat.isFile() 39 | iconClass = 'icon-file-text' 40 | else if stat.isDirectory() 41 | iconClass = 'icon-file-directory' 42 | catch 43 | iconClass = 'icon-file-text' 44 | 45 | """ 46 |
  • #{item.name}
  • 47 | """ 48 | 49 | listDir: (dir) -> 50 | result = [] 51 | 52 | try 53 | files = fs.readdirSync @resolveHome(dir) 54 | 55 | for f in files 56 | result.push(uri: path.join(dir, f), name: f) 57 | catch e 58 | console.warn "Unable to read directory #{dir}, #{e.message}" 59 | 60 | result 61 | 62 | renderItems: -> 63 | filePath = @filterEditorView.getText().trim() 64 | return [] if filePath is '' 65 | 66 | files = [] 67 | 68 | if @endWithSep filePath 69 | files = files.concat @listDir(filePath) 70 | 71 | if fs.existsSync @resolveHome(filePath) 72 | files.unshift 73 | name: "Open #{path.basename(filePath)} in new window" 74 | uri: filePath 75 | open: true 76 | else 77 | parentPath = @getParentPath filePath 78 | files = files.concat @listDir(parentPath) 79 | 80 | unless fs.existsSync @resolveHome(filePath) 81 | files.unshift 82 | name: "Create #{path.basename(filePath)}" 83 | uri: filePath 84 | 85 | files 86 | 87 | getParentPath: (filePath) -> 88 | if filePath is '~' 89 | filePath = process.env.HOME 90 | 91 | path.dirname filePath 92 | 93 | getFilterKey: -> 'uri' 94 | 95 | resolveHome: (filePath) -> 96 | if filePath[0] is '~' 97 | process.env.HOME + filePath.substring(1) 98 | else 99 | filePath 100 | 101 | toHome: (filePath) -> 102 | if filePath.indexOf(process.env.HOME) is 0 103 | filePath.replace process.env.HOME, '~' 104 | else 105 | filePath 106 | 107 | confirmed: (item) -> 108 | filePath = @resolveHome item.uri 109 | 110 | fs.stat filePath, (err, stats) => 111 | if err? or stats.isFile() 112 | atom.workspace.open filePath 113 | else if stats.isDirectory() 114 | if item.open? and item.open 115 | atom.open(pathsToOpen: [filePath]) 116 | else 117 | @filterEditorView.getEditor().setText(@ensureTailSep item.uri) 118 | 119 | endWithSep: (filePath) -> 120 | filePath[filePath.length - 1] is path.sep 121 | 122 | ensureTailSep: (f) -> 123 | if @endWithSep f then f else f + path.sep 124 | 125 | disableTab: -> 126 | @filterEditorView.on 'keydown', (evt) -> 127 | if evt.which is 9 128 | evt.stopPropagation() 129 | evt.preventDefault() 130 | -------------------------------------------------------------------------------- /lib/kill-ring-model.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore-plus' 2 | 3 | module.exports = 4 | class KillRing 5 | constructor: -> 6 | @reset() 7 | 8 | @capacity = 1000 9 | @emptyItem = 10 | text: '' 11 | meta: null 12 | 13 | _getCurrentItem: -> 14 | if @currentItemIndex >= 0 15 | @items[@currentItemIndex] 16 | else 17 | @emptyItem 18 | 19 | _gotoNextItem: -> 20 | return if @currentItemIndex is -1 21 | 22 | if @currentItemIndex is 0 23 | @currentItemIndex = @items.length - 1 24 | else 25 | @currentItemIndex-- 26 | 27 | reset: -> 28 | @items = [] 29 | @yanking = false 30 | @currentItemIndex = -1 31 | 32 | put: (text, meta) -> 33 | @items.push(text: text, meta: meta) 34 | @currentItemIndex = @items.length - 1 35 | 36 | yank: -> 37 | if @items.length 38 | @yanking = true 39 | @_getCurrentItem() 40 | else 41 | @emptyItem 42 | 43 | yankPop: -> 44 | if @yanking 45 | @_gotoNextItem() 46 | @_getCurrentItem() 47 | else 48 | throw new Error("Previous command is not yank.") 49 | 50 | yankText: -> @yank().text 51 | 52 | yankPopText: -> @yankPop().text 53 | 54 | cancel: -> 55 | @yanking = false if @yanking 56 | -------------------------------------------------------------------------------- /lib/kill-ring.coffee: -------------------------------------------------------------------------------- 1 | KillRingModel = require './kill-ring-model' 2 | {Range} = require 'atom' 3 | 4 | module.exports = 5 | model: new KillRingModel, 6 | 7 | enableKillRing: (editorView) -> 8 | return if editorView.hasClass 'kill-ring' 9 | 10 | editorView.command 'emacs:yank', => @yank editorView 11 | editorView.command 'emacs:yank-pop', => @yankPop editorView 12 | editorView.command 'emacs:kill-region', => @killRegion editorView 13 | editorView.command 'emacs:kill-ring-save', => 14 | @killRingSave editorView 15 | editorView.trigger 'emacs:clear-selection' 16 | 17 | editorView.command 'emacs:cancel-yank', => 18 | @cancelYank() 19 | 20 | editorView.on 'mouseup', => @killRingSave editorView 21 | editorView.on 'core:cancel', -> 22 | editorView.trigger 'emacs:cancel-yank' 23 | 24 | editorView.addClass 'kill-ring' 25 | 26 | 27 | yank: (editorView) -> 28 | @_saveClipboard() 29 | 30 | @_excludeCursor editorView, => 31 | editor = editorView.getEditor() 32 | @yankBeg = editor.getCursorBufferPosition() 33 | editor.insertText @model.yankText() 34 | 35 | yankPop: (editorView) -> 36 | @_excludeCursor editorView, => 37 | if @model.yanking 38 | editor = editorView.getEditor() 39 | text = @model.yankPopText() 40 | currentPos = editor.getCursorBufferPosition() 41 | editor.setTextInBufferRange(new Range(@yankBeg, currentPos), text) 42 | 43 | killRingSave: (editorView) -> 44 | @_saveClipboard() 45 | 46 | editor = editorView.getEditor() 47 | editor.copySelectedText() 48 | text = atom.clipboard.read() 49 | 50 | @model.put text 51 | editorView.trigger 'emacs:clear-mark' 52 | 53 | killRegion: (editorView) -> 54 | @_saveClipboard() 55 | 56 | editor = editorView.getEditor() 57 | editor.cutSelectedText() 58 | text = atom.clipboard.read() 59 | 60 | @model.put text 61 | 62 | cancelYank: -> 63 | @model.cancel() 64 | 65 | # need a better way to disable cursor while callback is executing 66 | _excludeCursor: (editorView, callback) -> 67 | editorView.off 'cursor:moved' 68 | 69 | callback.call @ 70 | 71 | setTimeout => 72 | editorView.on 'cursor:moved', => @cancelYank() 73 | 74 | _saveClipboard: -> 75 | text = atom.clipboard.read() 76 | 77 | return if text is 'initial clipboard content' 78 | 79 | if text isnt @model.yankText() 80 | @model.put text 81 | -------------------------------------------------------------------------------- /lib/mark.coffee: -------------------------------------------------------------------------------- 1 | {Range, Point} = require 'atom' 2 | 3 | MARKING = 'emacs-marking' 4 | 5 | module.exports = 6 | class EmacsMark 7 | constructor: (editorView) -> 8 | @editorView = editorView 9 | 10 | @editorView.command 'emacs:set-mark', => @toggleMark() 11 | @editorView.command 'emacs:clear-mark', => @clearMark() 12 | 13 | @editorView.on 'cursor:moved', => @extendSelection() 14 | @editorView.on 'core:cancel', => @clearMark() 15 | @editorView.getEditor().getBuffer().on 'changed', => @clearMark() 16 | 17 | @marking = false 18 | 19 | toggleMark: -> 20 | if @marking then @clearMark() else @setMark() 21 | 22 | setMark: -> 23 | @editorView.addClass(MARKING) 24 | @marking = true 25 | editor = @editorView.getEditor() 26 | 27 | @markBegin = editor.getCursorBufferPosition() 28 | 29 | clearMark: -> 30 | if @marking 31 | @marking = false 32 | @editorView.removeClass(MARKING) 33 | @editorView.trigger('emacs:clear-selection') 34 | 35 | extendSelection: -> 36 | return if not @marking 37 | 38 | editor = @editorView.getEditor() 39 | cursor = editor.getCursor() 40 | cursorPos = editor.getCursorBufferPosition() 41 | 42 | reverse = Point.min(cursorPos, @markBegin) is cursorPos 43 | 44 | cursor.selection.setBufferRange([@markBegin, cursorPos], isReversed: reverse) 45 | -------------------------------------------------------------------------------- /lib/switch-buffer-view.coffee: -------------------------------------------------------------------------------- 1 | {SelectListView} = require 'atom' 2 | 3 | 4 | class SwitchBufferView extends SelectListView 5 | initialize: -> 6 | super 7 | 8 | @addClass 'overlay from-top' 9 | editors = atom.workspace.getActivePane().getItems() 10 | items = (title: editor.getTitle(), uri: editor.getUri() || '' for editor in editors) 11 | items = @sortItems items 12 | 13 | @setItems items 14 | atom.workspaceView.appendToBottom this 15 | @focusFilterEditor() 16 | 17 | getFilterKey: -> 18 | return 'title' 19 | 20 | viewForItem: (item) -> 21 | """ 22 |
  • 23 |
    #{item.title}
    24 |
    #{item.uri}
    25 |
  • 26 | """ 27 | 28 | confirmed: (item) -> 29 | console.log("#{item.title} was selected") 30 | 31 | SwitchBufferView.lastItemUri = atom.workspace.getActiveEditor().getUri() 32 | atom.workspace.getActivePane().activateItemForUri(item.uri) 33 | @cancel() 34 | 35 | sortItems: (items) -> 36 | return items if not SwitchBufferView.lastItemUri? 37 | 38 | target = null 39 | newItems = [] 40 | 41 | for item in items 42 | if item.uri is SwitchBufferView.lastItemUri 43 | target = item 44 | else 45 | newItems.push item 46 | 47 | newItems.unshift target if target? 48 | newItems 49 | 50 | SwitchBufferView.lastItemUri = null 51 | 52 | module.exports = SwitchBufferView 53 | -------------------------------------------------------------------------------- /menus/emacs.cson: -------------------------------------------------------------------------------- 1 | 'menu': [ 2 | { 3 | 'label': 'Packages' 4 | 'submenu': [ 5 | 'label': 'Emacs Mode' 6 | 'submenu': [ 7 | { 'label': 'Toggle', 'command': 'emacs-mode:toggle' } 8 | ] 9 | ] 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emacs-mode", 3 | "main": "./lib/emacs", 4 | "version": "0.0.29", 5 | "description": "Enjoy a mouse-free Emacs experience on a modern editor.", 6 | "repository": "https://github.com/fuqcool/atom-emacs-mode.git", 7 | "license": "MIT", 8 | "engines": { 9 | "atom": ">0.50.0" 10 | }, 11 | "dependencies": { 12 | "underscore-plus": "^1.2.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spec/config-spec.coffee: -------------------------------------------------------------------------------- 1 | {WorkspaceView} = require 'atom' 2 | 3 | describe 'config', -> 4 | hideTabs = jasmine.createSpy 'hideTabs' 5 | hideSidebar = jasmine.createSpy 'hideSidebar' 6 | useEmacsCursor = jasmine.createSpy 'useEmacsCursor' 7 | useFuzzyFileFinder = jasmine.createSpy 'useFuzzyFileFinder' 8 | useFuzzyBufferFinder = jasmine.createSpy 'useFuzzyBufferFinder' 9 | 10 | beforeEach -> 11 | atom.config.set 'emacs-mode.hideTabs', undefined 12 | atom.config.set 'emacs-mode.hideSidebar', undefined 13 | atom.config.set 'emacs-mode.useEmacsCursor', undefined 14 | atom.config.set 'emacs-mode.useFuzzyFileFinder', undefined 15 | atom.config.set 'emacs-mode.useFuzzyBufferFinder', undefined 16 | 17 | atom.workspaceView = new WorkspaceView 18 | atom.workspaceView.on 'emacs:hide-tabs', hideTabs 19 | atom.workspaceView.on 'emacs:hide-sidebar', hideSidebar 20 | atom.workspaceView.on 'emacs:use-emacs-cursor', useEmacsCursor 21 | atom.workspaceView.on 'emacs:use-fuzzy-file-finder', useFuzzyFileFinder 22 | atom.workspaceView.on 'emacs:use-fuzzy-buffer-finder', useFuzzyBufferFinder 23 | 24 | require '../lib/config' 25 | 26 | it 'should ensure config exists', -> 27 | expect(atom.config.get('emacs-mode.hideTabs')).toBeDefined() 28 | expect(atom.config.get('emacs-mode.hideSidebar')).toBeDefined() 29 | expect(atom.config.get('emacs-mode.useEmacsCursor')).toBeDefined() 30 | expect(atom.config.get('emacs-mode.useFuzzyFileFinder')).toBeDefined() 31 | expect(atom.config.get('emacs-mode.useFuzzyBufferFinder')).toBeDefined() 32 | 33 | it 'should trigger corresponding events', -> 34 | expect(hideTabs).toHaveBeenCalled() 35 | expect(hideSidebar).toHaveBeenCalled() 36 | expect(useEmacsCursor).toHaveBeenCalled() 37 | expect(useFuzzyFileFinder).toHaveBeenCalled() 38 | expect(useFuzzyBufferFinder).toHaveBeenCalled() 39 | -------------------------------------------------------------------------------- /spec/config-util-spec.coffee: -------------------------------------------------------------------------------- 1 | configUtil = require '../lib/config-util' 2 | 3 | describe 'config util', -> 4 | watchHandler = null 5 | keyPath = 'emacs-mode:foo' 6 | 7 | getConfig = -> atom.config.get keyPath 8 | setConfig = (value) -> atom.config.set keyPath, value 9 | 10 | beforeEach -> 11 | setConfig null 12 | watchHandler = jasmine.createSpy 'watchHandler' 13 | 14 | it 'should write default value if key does not exists', -> 15 | configUtil.watch keyPath, false, watchHandler 16 | 17 | expect(getConfig()).toBe(false) 18 | expect(watchHandler).toHaveBeenCalledWith(false) 19 | 20 | it 'should not write default value if key exists', -> 21 | setConfig false 22 | configUtil.watch keyPath, true, watchHandler 23 | 24 | expect(watchHandler).toHaveBeenCalledWith(false) 25 | expect(getConfig()).toBe(false) 26 | 27 | it 'should work with only two arguments', -> 28 | configUtil.watch keyPath, watchHandler 29 | setConfig true 30 | 31 | expect(watchHandler).toHaveBeenCalledWith(true) 32 | 33 | it 'should call handler with null if no config exists', -> 34 | configUtil.watch keyPath, watchHandler 35 | 36 | expect(watchHandler).toHaveBeenCalledWith(null) 37 | 38 | it 'should not throw error if there is only one argument', -> 39 | expect(-> 40 | configUtil.watch keyPath 41 | ).not.toThrow() 42 | -------------------------------------------------------------------------------- /spec/find-file-view-spec.coffee: -------------------------------------------------------------------------------- 1 | {WorkspaceView} = require 'atom' 2 | fs = require 'fs' 3 | path = require 'path' 4 | 5 | FileFinderView = require '../lib/find-file-view' 6 | 7 | describe 'file finder view', -> 8 | context = (uri) -> 9 | atom.workspace = { 10 | getActiveEditor: -> {getUri: -> uri} 11 | } 12 | 13 | beforeEach -> 14 | path.sep = '/' 15 | process.env.HOME = '/home/me' 16 | 17 | atom.workspaceView = new WorkspaceView 18 | 19 | describe 'initialization', -> 20 | beforeEach -> 21 | spyOn(fs, 'existsSync').andReturn true 22 | 23 | it 'uses HOME as default directory when no file is open', -> 24 | context '' 25 | spyOn(fs, 'readdirSync').andReturn [] 26 | 27 | fileFinderView = new FileFinderView 28 | 29 | expect(fileFinderView.filterEditorView.getText()).toBe '~/' 30 | 31 | it 'uses HOME as default directory when file has no path', -> 32 | context '.' 33 | spyOn(fs, 'readdirSync').andReturn [] 34 | 35 | fileFinderView = new FileFinderView 36 | 37 | expect(fileFinderView.filterEditorView.getText()).toBe '~/' 38 | 39 | 40 | it 'finds file in parent directory', -> 41 | context '/a/b/c' 42 | spyOn(fs, 'readdirSync').andReturn [] 43 | 44 | fileFinderView = new FileFinderView 45 | 46 | expect(fileFinderView.filterEditorView.getText()).toBe '/a/b/' 47 | 48 | it 'finds file in parent directory in home', -> 49 | context '/home/me/a/b' 50 | spyOn(fs, 'readdirSync').andReturn [] 51 | 52 | fileFinderView = new FileFinderView 53 | 54 | expect(fileFinderView.filterEditorView.getText()).toBe '~/a/' 55 | -------------------------------------------------------------------------------- /spec/kill-ring-model-spec.coffee: -------------------------------------------------------------------------------- 1 | KillRing = require '../lib/kill-ring-model' 2 | 3 | describe 'kill ring model', -> 4 | killRing = null 5 | 6 | beforeEach -> 7 | killRing = new KillRing 8 | 9 | describe 'yank', -> 10 | it 'should perform simple paste', -> 11 | killRing.put 'foo' 12 | 13 | item = killRing.yank() 14 | expect(item.text).toBe 'foo' 15 | 16 | it 'should paste the latest text', -> 17 | killRing.put 'foo' 18 | killRing.put 'bar' 19 | 20 | item = killRing.yank() 21 | expect(item.text).toBe 'bar' 22 | 23 | it 'should return empty item if there is no item in the killring', -> 24 | item = killRing.yank() 25 | expect(item.text).toBe '' 26 | 27 | describe 'yank pop', -> 28 | it 'should raise an error if previous command is not yank', -> 29 | killRing.put 'foo' 30 | 31 | expect(-> killRing.yankPop()).toThrow() 32 | 33 | it 'should go through the kill ring over again', -> 34 | killRing.put 'foo' 35 | killRing.put 'bar' 36 | killRing.yank() 37 | 38 | item = killRing.yankPop() 39 | expect(item.text).toBe 'foo' 40 | 41 | item = killRing.yankPop() 42 | expect(item.text).toBe 'bar' 43 | 44 | it 'should work when there is only one item in the kill ring', -> 45 | killRing.put 'foo' 46 | killRing.yank() 47 | 48 | killRing.yankPop() 49 | item = killRing.yankPop() 50 | 51 | expect(item.text).toBe 'foo' 52 | 53 | it 'should take the lastest poped item as top', -> 54 | killRing.put 'foo' 55 | killRing.put 'bar' 56 | 57 | killRing.yank() 58 | killRing.yankPop() 59 | 60 | item = killRing.yank() 61 | 62 | expect(item.text).toBe 'foo' 63 | 64 | it 'should cancel a yanking', -> 65 | killRing.put 'foo' 66 | killRing.put 'bar' 67 | 68 | killRing.yank() 69 | killRing.yankPop() 70 | killRing.cancel() 71 | 72 | expect(-> killRing.yankPop()).toThrow() 73 | 74 | it 'should reset index to top when putting new item', -> 75 | killRing.put 'foo' 76 | killRing.put 'bar' 77 | 78 | killRing.yank() 79 | killRing.yankPop() 80 | 81 | killRing.put 'bla' 82 | 83 | item = killRing.yank() 84 | expect(item.text).toBe 'bla' 85 | 86 | item = killRing.yankPop() 87 | expect(item.text).toBe 'bar' 88 | 89 | it 'should store meta info together with text', -> 90 | killRing.put 'foo', {start: 100} 91 | item = killRing.yank() 92 | 93 | expect(item.meta.start).toBe 100 94 | 95 | it 'reset kill ring', -> 96 | killRing.put 'foo' 97 | killRing.yank() 98 | 99 | killRing.reset() 100 | expect(killRing.items.length).toBe 0 101 | expect(killRing.yanking).toBe false 102 | expect(killRing.currentItemIndex).toBe -1 103 | -------------------------------------------------------------------------------- /spec/kill-ring-spec.coffee: -------------------------------------------------------------------------------- 1 | {WorkspaceView, EditorView, Range} = require 'atom' 2 | killRing = require '../lib/kill-ring' 3 | 4 | describe 'kill-ring', -> 5 | editorView = null 6 | editor = null 7 | 8 | beforeEach -> 9 | session = atom.project.openSync() 10 | editorView = new EditorView(session) 11 | 12 | editor = editorView.getEditor() 13 | editor.setText 'abcde' 14 | 15 | killRing.model.reset() 16 | killRing.enableKillRing editorView 17 | 18 | atom.clipboard.write 'initial clipboard content' 19 | 20 | it 'has class attached', -> 21 | expect(editorView.hasClass('kill-ring')).toBe true 22 | 23 | it 'copies text', -> 24 | editor.setSelectedBufferRange(new Range([0, 0], [0, 3])) 25 | editorView.trigger 'emacs:kill-ring-save' 26 | 27 | text = atom.clipboard.read() 28 | 29 | expect(text).toBe 'abc' 30 | expect(editor.getText()).toBe 'abcde' 31 | 32 | it 'cuts text', -> 33 | editor.setSelectedBufferRange(new Range([0, 2], [0, 5])) 34 | editorView.trigger 'emacs:kill-region' 35 | 36 | text = atom.clipboard.read() 37 | 38 | expect(text).toBe 'cde' 39 | expect(editor.getText()).toBe 'ab' 40 | 41 | it 'pastes text', -> 42 | editor.setSelectedBufferRange(new Range([0, 0], [0, 3])) 43 | editorView.trigger 'emacs:kill-region' 44 | 45 | text = atom.clipboard.read() 46 | editor.moveCursorToEndOfLine() 47 | 48 | editorView.trigger 'emacs:yank' 49 | 50 | expect(editor.getText()).toBe 'deabc' 51 | 52 | editorView.trigger 'emacs:yank' 53 | 54 | expect(editorView.getText()).toBe 'deabcabc' 55 | 56 | it 'searches kill ring', -> 57 | editor.setSelectedBufferRange(new Range([0, 0], [0, 1])) 58 | editorView.trigger 'emacs:kill-region' 59 | 60 | editor.setSelectedBufferRange(new Range([0, 0], [0, 4])) 61 | editorView.trigger 'emacs:kill-region' 62 | 63 | editorView.trigger 'emacs:yank' 64 | expect(editor.getText()).toBe 'bcde' 65 | 66 | editorView.trigger 'emacs:yank-pop' 67 | expect(editor.getText()).toBe 'a' 68 | 69 | editorView.trigger 'emacs:yank-pop' 70 | expect(editor.getText()).toBe 'bcde' 71 | 72 | it 'copies text by mouse', -> 73 | editor.setSelectedBufferRange(new Range([0, 0], [0, 3])) 74 | 75 | editorView.trigger 'mouseup' 76 | 77 | text = atom.clipboard.read() 78 | expect(text).toBe 'abc' 79 | 80 | it 'paste text from outside atom', -> 81 | editor.setText '' 82 | atom.clipboard.write 'alien' 83 | 84 | editorView.trigger 'emacs:yank' 85 | 86 | expect(editor.getText()).toBe 'alien' 87 | -------------------------------------------------------------------------------- /stylesheets/emacs.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/stylesheets/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .editor.emacs-cursor .cursor { 8 | background-color: white; 9 | opacity: 0.8; 10 | border: none; 11 | } 12 | 13 | .editor.mini.emacs-cursor .cursor { 14 | width: 10px; 15 | } 16 | --------------------------------------------------------------------------------