├── .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 [](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 |
--------------------------------------------------------------------------------