Compare commits

..

No commits in common. "master" and "v0.1.1" have entirely different histories.

28 changed files with 157 additions and 2624 deletions

View file

@ -1,9 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{coffee,json}]
indent_style = space
indent_size = 2

View file

@ -1,23 +0,0 @@
# Ex-Mode Contributing Guidelines
Current Maintainers:
- [@jazzpi](https://github.com/jazzpi)
This project is accepting new maintainers. Interested parties should open a new issue titled `New Maintainer Request`.
## Pull Requests
- If the PR *fixes* or should result in the closure of any issues, use the `fixes #` or `closes #` syntax to ensure issue will
close when your PR is merged
- All pull-requests that fix a bug or add a new feature *must* have accompanying tests before they will be merged. If you want
to speed up the merge of your PR, please contribute these tests
- *note*: if you submit a PR but are unsure how to write tests, please begin your PR title with `[needs tests]`
- Please use the [pull request template](PULL_REQUEST_TEMPLATE.md) as a guide for submitting your PR.
## Issues
- Be aware of the responsibilities of `ex-mode` vs `vim-mode`
- If you have identified a bug we would welcome any Pull Requests that either:
- Fix the issue
- Create failing tests to confirm the bug

View file

@ -1,15 +0,0 @@
Fixes # .
Changes Proposed in this Pull Request:
- foo
- bar
- baz
I have written tests for:
[](Remove the `[]()` to uncomment the appropriate lines)
[](- New features introduced)
[](- Bugs fixed)
[](- Neither (I'm just enhancing tests!))

View file

@ -1,45 +0,0 @@
## Project specific config ###
language: generic
env:
global:
- APM_TEST_PACKAGES="vim-mode-plus"
- ATOM_LINT_WITH_BUNDLED_NODE="true"
matrix:
- ATOM_CHANNEL=stable
- ATOM_CHANNEL=beta
os:
- linux
dist: trusty
### Generic setup follows ###
script:
- curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh
- chmod u+x build-package.sh
- ./build-package.sh
notifications:
email:
on_success: never
on_failure: change
branches:
only:
- master
git:
depth: 10
sudo: false
addons:
apt:
packages:
- build-essential
- git
- libgnome-keyring-dev
- libsecret-1-dev
- fakeroot

View file

@ -1,93 +1,3 @@
## (unpublished)
* Fix `:enew` not working ([#215](https://github.com/lloeki/ex-mode/pull/215))
## 0.18.0
* Add Gdefault support ([#191](https://github.com/lloeki/ex-mode/pull/191))
* Add :sort command ([#190](https://github.com/lloeki/ex-mode/pull/190))
## 0.17.0
* Add support for Atom 1.19 ([#185](https://github.com/lloeki/ex-mode/pull/185))
* Added support for canceling ex-mode with Ctrl-C ([#186](https://github.com/lloeki/ex-mode/pull/186))
## 0.16.0
* Support for Atom 1.18 and 1.19 ([#184](https://github.com/lloeki/ex-mode/pull/184))
## 0.15.0
* `vim-mode-plus` support!
- Add keybinding (@jmarianer)
- Support `:substitute` command (@mkiken)
- Support marks
- Use `vim-mode-plus` in specs
## 0.14.0
* Support `:tabonly` (@jmarianer)
* Fix `:x` closing Atom instead of the current pane
## 0.13.1
* Limit addresses to the last line
* Fix autocompleting a non existent directory (@mcnicholls)
## 0.13.0
* Added basic support for visual marks (e.g. `:'<,'>s/foo/bar`)
* Added `smartcase` option to `:set`
* Fixed using marks as addresses (e.g. `:'a,.delete`)
* Fixed search not working without a closing delimiter (e.g. `:/foo`)
## 0.12.0
* Added file and command autocomplete (@stuartquin)
* Added `splitbelow` and `splitright` options to `:set`
* Fixed the editor not updating when saving a new file with `:w` or `:saveas`
## 0.11.0
* Stop using non-standard Promise.defer (fixes issue with `:w`) (@AsaAyers)
## 0.9.0
* Added support for yank commands, ex `:1,10y` (@posgarou)
* Added contributor guidelines, including a pull request template
* Added ability to control splitting with `splitright`, and `splitbelow` (@dragonxwang)
### Fixes
* delete commands now add text to clipboard, ex `:1,4d`
## 0.8.0
* Don't allow :s delimiters not allowed by vim (@jacwah)
* Backspace over empty `:` now cancels ex-mode (@shamrin)
* Added option to register alias keys in atom init config (@GertjanReynaert)
* Allow `:substitute` to replace empty with an empty string and replacing the last search item (@jazzpi)
* Added `:wall`, `:quitall` and `:wqall` commands (@caiocutrim)
* Added `:saveas` command (@bakert)
## 0.6.0
* No project/multiple projects paths (uses first one)
* Support for :set
* Fixes
## 0.5.0
* Comply with upcoming Atom API 1.0
* Added `:d`
* Fixes
## 0.4.1 - C-C-C-Combo Edition
* Added ex command parser, including ranges
* Added `:wq`
* Added `:s`
* Alert on unknown or invalid command
## 0.3.0 - Extrovert Edition
* Register new commands from the outside world
* Added `:tabn`, `:tabp`, `:e`, `:enew`, and a few aliases
## 0.2.0 - NotAOneTrickPony Edition
* Added `:quit`, `:tabedit`, `:wa`, `:split`, `:vsplit`
* Commands can take arguments
## 0.1.0 - First Release ## 0.1.0 - First Release
* Every feature added * Every feature added
* Every bug fixed * Every bug fixed

63
README.md Executable file → Normal file
View file

@ -2,67 +2,10 @@
ex-mode for Atom's vim-mode ex-mode for Atom's vim-mode
## Use ## Usage
Install both [vim-mode-plus](https://github.com/t9md/atom-vim-mode-plus) (or Install vim-mode. Type `:` in command mode. Enter `w`. or `write`.
the deprecated `vim-mode`) and ex-mode. Type `:` in command mode. Enter `w` or
`write`.
## Extend
Use the service to register commands, from your own package, or straight from `init.coffee`:
```coffee
# in Atom's init.coffee
atom.packages.onDidActivatePackage (pack) ->
if pack.name == 'ex-mode'
Ex = pack.mainModule.provideEx()
Ex.registerCommand 'z', -> console.log("Zzzzzz...")
```
You can also add aliases:
```coffee
atom.packages.onDidActivatePackage (pack) ->
if pack.name == 'ex-mode'
Ex = pack.mainModule.provideEx()
Ex.registerAlias 'WQ', 'wq'
Ex.registerAlias 'Wq', 'wq'
```
## Existing commands
This is the baseline list of commands supported in `ex-mode`.
| Command | Operation |
| --------------------------------------- | ---------------------------------- |
| `q/quit/tabc/tabclose` | Close active tab |
| `qall/quitall` | Close all tabs |
| `tabe/tabedit/tabnew` | Open new tab |
| `e/edit/tabe/tabedit/tabnew <file>` | Edit given file |
| `tabn/tabnext` | Go to next tab |
| `tabp/tabprevious` | Go to previous tab |
| `tabo/tabonly` | Close other tabs |
| `w/write` | Save active tab |
| `w/write/saveas <file>` | Save as |
| `wall/wa` | Save all tabs |
| `sp/split` | Split window |
| `sp/split <file>` | Open file in split window |
| `s/substitute` | Substitute regular expression in active line |
| `vsp/vsplit` | Vertical split window |
| `vsp/vsplit <file>` | Open file in vertical split window |
| `delete` | Cut active line |
| `yank` | Copy active line |
| `set <options>` | Set options |
| `sort` | Sort all lines in file |
| `sort <line range>` | Sort lines in line range |
See `lib/ex.coffee` for the implementations of these commands. Contributions are very welcome!
## Status ## Status
Groundwork is done. More ex commands are easy to add and will be coming as time permits and contributions come in. Groundwork is done. More ex commands are easy to add and will be coming soon.
## License
MIT

View file

@ -7,10 +7,5 @@
# For more detailed documentation see # For more detailed documentation see
# https://atom.io/docs/latest/advanced/keymaps # https://atom.io/docs/latest/advanced/keymaps
'atom-text-editor.vim-mode-plus:not(.insert-mode)':
':': 'ex-mode:open'
'atom-text-editor.ex-mode-editor':
'ctrl-c': 'ex-mode:close'
'ctrl-[': 'ex-mode:close'
'atom-text-editor.vim-mode:not(.insert-mode)': 'atom-text-editor.vim-mode:not(.insert-mode)':
':': 'ex-mode:open' ':': 'ex-mode:open'

View file

@ -1,80 +0,0 @@
fs = require 'fs'
path = require 'path'
os = require 'os'
Ex = require './ex'
module.exports =
class AutoComplete
constructor: (commands) ->
@commands = commands
@resetCompletion()
resetCompletion: () ->
@autoCompleteIndex = 0
@autoCompleteText = null
@completions = []
expandTilde: (filePath) ->
if filePath.charAt(0) == '~'
return os.homedir() + filePath.slice(1)
else
return filePath
getAutocomplete: (text) ->
if !@autoCompleteText
@autoCompleteText = text
parts = @autoCompleteText.split(' ')
cmd = parts[0]
if parts.length > 1
filePath = parts.slice(1).join(' ')
return @getCompletion(() => @getFilePathCompletion(cmd, filePath))
else
return @getCompletion(() => @getCommandCompletion(cmd))
filterByPrefix: (commands, prefix) ->
commands.sort().filter((f) => f.startsWith(prefix))
getCompletion: (completeFunc) ->
if @completions.length == 0
@completions = completeFunc()
complete = ''
if @completions.length
complete = @completions[@autoCompleteIndex % @completions.length]
@autoCompleteIndex++
# Only one result so lets return this directory
if complete.endsWith('/') && @completions.length == 1
@resetCompletion()
return complete
getCommandCompletion: (command) ->
return @filterByPrefix(@commands, command)
getFilePathCompletion: (command, filePath) ->
filePath = @expandTilde(filePath)
if filePath.endsWith(path.sep)
basePath = path.dirname(filePath + '.')
baseName = ''
else
basePath = path.dirname(filePath)
baseName = path.basename(filePath)
try
basePathStat = fs.statSync(basePath)
if basePathStat.isDirectory()
files = fs.readdirSync(basePath)
return @filterByPrefix(files, baseName).map((f) =>
filePath = path.join(basePath, f)
if fs.lstatSync(filePath).isDirectory()
return command + ' ' + filePath + path.sep
else
return command + ' ' + filePath
)
return []
catch err
return []

View file

@ -1,5 +0,0 @@
class CommandError
constructor: (@message) ->
@name = 'Command Error'
module.exports = CommandError

View file

@ -1,201 +1,21 @@
ExViewModel = require './ex-view-model' ExViewModel = require './ex-view-model'
Ex = require './ex' Ex = require './ex'
Find = require './find'
CommandError = require './command-error' class CommandError
constructor: (@message) ->
@name = 'Command Error'
class Command class Command
constructor: (@editor, @exState) -> constructor: (@editor, @exState) ->
@selections = @exState.getSelections() @viewModel = new ExViewModel(@)
@viewModel = new ExViewModel(@, Object.keys(@selections).length > 0)
parseAddr: (str, cursor) ->
row = cursor.getBufferRow()
if str is '.'
addr = row
else if str is '$'
# Lines are 0-indexed in Atom, but 1-indexed in vim.
# The two ways of getting length let us support Atom 1.19's new buffer
# implementation (https://github.com/atom/atom/pull/14435) and still
# support 1.18 and below
buffer = @editor.getBuffer()
addr = (buffer.getLineCount?() ? buffer.lines.length) - 1
else if str[0] in ["+", "-"]
addr = row + @parseOffset(str)
else if not isNaN(str)
addr = parseInt(str) - 1
else if str[0] is "'" # Parse Mark...
unless @vimState?
throw new CommandError("Couldn't get access to vim-mode.")
mark = @vimState.mark.marks[str[1]]
unless mark?
throw new CommandError("Mark #{str} not set.")
addr = mark.getEndBufferPosition().row
else if str[0] is "/"
str = str[1...]
if str[str.length-1] is "/"
str = str[...-1]
addr = Find.scanEditor(str, @editor, cursor.getCurrentLineBufferRange().end)[0]
unless addr?
throw new CommandError("Pattern not found: #{str}")
addr = addr.start.row
else if str[0] is "?"
str = str[1...]
if str[str.length-1] is "?"
str = str[...-1]
addr = Find.scanEditor(str, @editor, cursor.getCurrentLineBufferRange().start, true)[0]
unless addr?
throw new CommandError("Pattern not found: #{str[1...-1]}")
addr = addr.start.row
return addr
parseOffset: (str) ->
if str.length is 0
return 0
if str.length is 1
o = 1
else
o = parseInt(str[1..])
if str[0] is '+'
return o
else
return -o
execute: (input) -> execute: (input) ->
@vimState = @exState.globalExState.vim?.getEditorState(@editor) return unless input.characters.length > 0
# Command line parsing (mostly) following the rules at
# http://pubs.opengroup.org/onlinepubs/9699919799/utilities
# /ex.html#tag_20_40_13_03
# Steps 1/2: Leading blanks and colons are ignored.
cl = input.characters
cl = cl.replace(/^(:|\s)*/, '')
return unless cl.length > 0
# Step 3: If the first character is a ", ignore the rest of the line
if cl[0] is '"'
return
# Step 4: Address parsing
# see comment in parseAddr about line length
buffer = @editor.getBuffer()
lastLine = (buffer.getLineCount?() ? buffer.lines.length) - 1
if cl[0] is '%'
range = [0, lastLine]
cl = cl[1..]
else
addrPattern = ///^
(?: # First address
(
\.| # Current line
\$| # Last line
\d+| # n-th line
'[\[\]<>'`"^.(){}a-zA-Z]| # Marks
/.*?(?:[^\\]/|$)| # Regex
\?.*?(?:[^\\]\?|$)| # Backwards search
[+-]\d* # Current line +/- a number of lines
)((?:\s*[+-]\d*)*) # Line offset
)?
(?:, # Second address
( # Same as first address
\.|
\$|
\d+|
'[\[\]<>'`"^.(){}a-zA-Z]|
/.*?[^\\]/|
\?.*?[^\\]\?|
[+-]\d*
)((?:\s*[+-]\d*)*)
)?
///
[match, addr1, off1, addr2, off2] = cl.match(addrPattern)
cursor = @editor.getLastCursor()
# Special case: run command on selection. This can't be handled by simply
# parsing the mark since vim-mode doesn't set it (and it would be fairly
# useless with multiple selections)
if addr1 is "'<" and addr2 is "'>"
runOverSelections = true
else
runOverSelections = false
if addr1?
address1 = @parseAddr(addr1, cursor)
else
# If no addr1 is given (,+3), assume it is '.'
address1 = cursor.getBufferRow()
if off1?
address1 += @parseOffset(off1)
address1 = 0 if address1 is -1
address1 = lastLine if address1 > lastLine
if address1 < 0
throw new CommandError('Invalid range')
if addr2?
address2 = @parseAddr(addr2, cursor)
if off2?
address2 += @parseOffset(off2)
address2 = 0 if address2 is -1
address2 = lastLine if address2 > lastLine
if address2 < 0
throw new CommandError('Invalid range')
if address2 < address1
throw new CommandError('Backwards range given')
range = [address1, if address2? then address2 else address1]
cl = cl[match?.length..]
# Step 5: Leading blanks are ignored
cl = cl.trimLeft()
# Step 6a: If no command is specified, go to the last specified address
if cl.length is 0
@editor.setCursorBufferPosition([range[1], 0])
return
# Ignore steps 6b and 6c since they only make sense for print commands and
# print doesn't make sense
# Ignore step 7a since flags are only useful for print
# Step 7b: :k<valid mark> is equal to :mark <valid mark> - only a-zA-Z is
# in vim-mode for now
if cl.length is 2 and cl[0] is 'k' and /[a-z]/i.test(cl[1])
command = 'mark'
args = cl[1]
else if not /[a-z]/i.test(cl[0])
command = cl[0]
args = cl[1..]
else
[m, command, args] = cl.match(/^(\w+)(.*)/)
# If the command matches an existing one exactly, execute that one
unless (func = Ex.singleton()[command])?
# Step 8: Match command against existing commands
matching = (name for name, val of Ex.singleton() when \
name.indexOf(command) is 0)
matching.sort()
command = matching[0]
func = Ex.singleton()[command]
func = (new Ex)[input.characters]
if func? if func?
if runOverSelections func()
for id, selection of @selections
bufferRange = selection.getBufferRange()
range = [bufferRange.start.row, bufferRange.end.row]
func({ range, args, @vimState, @exState, @editor })
else
func({ range, args, @vimState, @exState, @editor })
else else
throw new CommandError("Not an editor command: #{input.characters}") throw new CommandError("#{input.characters}")
module.exports = Command module.exports = {Command, CommandError}

View file

@ -0,0 +1,58 @@
{View, TextEditorView} = require 'atom'
module.exports =
class ExCommandModeInputView extends View
@content: ->
@div class: 'command-mode-input', =>
@div class: 'editor-container', outlet: 'editorContainer', =>
@subview 'editor', new TextEditorView(mini: true)
initialize: (@viewModel, opts = {})->
if opts.class?
@editorContainer.addClass opts.class
if opts.hidden
@editorContainer.addClass 'hidden-input'
@singleChar = opts.singleChar
@defaultText = opts.defaultText ? ''
@panel = atom.workspace.addBottomPanel(item: this, priority: 100)
@focus()
@handleEvents()
handleEvents: ->
if @singleChar?
@editor.find('input').on 'textInput', @autosubmit
@editor.on 'core:confirm', @confirm
@editor.on 'core:cancel', @cancel
@editor.find('input').on 'blur', @cancel
stopHandlingEvents: ->
if @singleChar?
@editor.find('input').off 'textInput', @autosubmit
@editor.off 'core:confirm', @confirm
@editor.off 'core:cancel', @cancel
@editor.find('input').off 'blur', @cancel
autosubmit: (event) =>
@editor.setText(event.originalEvent.data)
@confirm()
confirm: =>
@value = @editor.getText() or @defaultText
@viewModel.confirm(@)
@remove()
focus: =>
@editorContainer.find('.editor').focus()
cancel: (e) =>
@viewModel.cancel(@)
@remove()
remove: =>
@stopHandlingEvents()
atom.workspace.getActivePane().activate()
@panel.destroy()

View file

@ -1,6 +1,5 @@
GlobalExState = require './global-ex-state' GlobalExState = require './global-ex-state'
ExState = require './ex-state' ExState = require './ex-state'
Ex = require './ex'
{Disposable, CompositeDisposable} = require 'event-kit' {Disposable, CompositeDisposable} = require 'event-kit'
module.exports = ExMode = module.exports = ExMode =
@ -27,36 +26,3 @@ module.exports = ExMode =
deactivate: -> deactivate: ->
@disposables.dispose() @disposables.dispose()
provideEx: ->
registerCommand: Ex.registerCommand.bind(Ex)
registerAlias: Ex.registerAlias.bind(Ex)
consumeVim: (vim) ->
@vim = vim
@globalExState.setVim(vim)
consumeVimModePlus: (vim) ->
this.consumeVim(vim)
config:
splitbelow:
title: 'Split below'
description: 'when splitting, split from below'
type: 'boolean'
default: 'false'
splitright:
title: 'Split right'
description: 'when splitting, split from right'
type: 'boolean'
default: 'false'
gdefault:
title: 'Gdefault'
description: 'When on, the ":substitute" flag \'g\' is default on'
type: 'boolean'
default: 'false'
onlyCloseBuffers:
title: 'Only close buffers'
description: 'When on, quitall only closes all buffers, not entire Atom instance'
type: 'boolean'
default: 'false'

View file

@ -1,71 +0,0 @@
class ExCommandModeInputElement extends HTMLDivElement
createdCallback: ->
@className = "command-mode-input"
@editorContainer = document.createElement("div")
@editorContainer.className = "editor-container"
@appendChild(@editorContainer)
initialize: (@viewModel, opts = {}) ->
if opts.class?
@editorContainer.classList.add(opts.class)
if opts.hidden
@editorContainer.style.height = "0px"
@editorElement = document.createElement "atom-text-editor"
@editorElement.classList.add('editor') # Consider this deprecated!
@editorElement.classList.add('ex-mode-editor')
@editorElement.getModel().setMini(true)
@editorElement.setAttribute('mini', '')
@editorContainer.appendChild(@editorElement)
@singleChar = opts.singleChar
@defaultText = opts.defaultText ? ''
@panel = atom.workspace.addBottomPanel(item: this, priority: 100)
@focus()
@handleEvents()
this
handleEvents: ->
if @singleChar?
@editorElement.getModel().getBuffer().onDidChange (e) =>
@confirm() if e.newText
else
atom.commands.add(@editorElement, 'editor:newline', @confirm.bind(this))
atom.commands.add(@editorElement, 'core:backspace', @backspace.bind(this))
atom.commands.add(@editorElement, 'core:confirm', @confirm.bind(this))
atom.commands.add(@editorElement, 'core:cancel', @cancel.bind(this))
atom.commands.add(@editorElement, 'ex-mode:close', @cancel.bind(this))
atom.commands.add(@editorElement, 'blur', @cancel.bind(this))
backspace: ->
# pressing backspace over empty `:` should cancel ex-mode
@cancel() unless @editorElement.getModel().getText().length
confirm: ->
@value = @editorElement.getModel().getText() or @defaultText
@viewModel.confirm(this)
@removePanel()
focus: ->
@editorElement.focus()
cancel: (e) ->
@viewModel.cancel(this)
@removePanel()
removePanel: ->
atom.workspace.getActivePane().activate()
@panel.destroy()
module.exports =
document.registerElement("ex-command-mode-input"
extends: "div",
prototype: ExCommandModeInputElement.prototype
)

View file

@ -1,7 +1,6 @@
{Emitter, Disposable, CompositeDisposable} = require 'event-kit' {Emitter, Disposable, CompositeDisposable} = require 'event-kit'
Command = require './command' {Command, CommandError} = require './command'
CommandError = require './command-error'
class ExState class ExState
constructor: (@editorElement, @globalExState) -> constructor: (@editorElement, @globalExState) ->
@ -34,9 +33,6 @@ class ExState
onDidFailToExecute: (fn) -> onDidFailToExecute: (fn) ->
@emitter.on('failed-to-execute', fn) @emitter.on('failed-to-execute', fn)
onDidProcessOpStack: (fn) ->
@emitter.on('processed-op-stack', fn)
pushOperations: (operations) -> pushOperations: (operations) ->
@opStack.push operations @opStack.push operations
@ -48,25 +44,14 @@ class ExState
processOpStack: -> processOpStack: ->
[command, input] = @opStack [command, input] = @opStack
if input.characters.length > 0 if input.characters.length > 0
@history.unshift command
try try
command.execute(input) command.execute(input)
@history.unshift command
catch e catch e
if (e instanceof CommandError) if (e instanceof CommandError)
atom.notifications.addError("Command error: #{e.message}")
@emitter.emit('failed-to-execute') @emitter.emit('failed-to-execute')
else else
throw e throw e
@clearOpStack() @clearOpStack()
@emitter.emit('processed-op-stack')
# Returns all non-empty selections
getSelections: ->
filtered = {}
for id, selection of @editor.getSelections()
unless selection.isEmpty()
filtered[id] = selection
return filtered
module.exports = ExState module.exports = ExState

View file

@ -1,41 +1,20 @@
{ViewModel, Input} = require './view-model' {ViewModel, Input} = require './view-model'
AutoComplete = require './autocomplete'
Ex = require './ex'
module.exports = module.exports =
class ExViewModel extends ViewModel class ExViewModel extends ViewModel
constructor: (@exCommand, withSelection) -> constructor: (@exCommand) ->
super(@exCommand, class: 'command') super(@exCommand, class: 'command')
@historyIndex = -1 @historyIndex = -1
if withSelection @view.editor.on('core:move-up', @increaseHistoryEx)
@view.editorElement.getModel().setText("'<,'>") @view.editor.on('core:move-down', @decreaseHistoryEx)
@view.editorElement.addEventListener('keydown', @tabAutocomplete)
atom.commands.add(@view.editorElement, 'core:move-up', @increaseHistoryEx)
atom.commands.add(@view.editorElement, 'core:move-down', @decreaseHistoryEx)
@autoComplete = new AutoComplete(Ex.getCommands())
restoreHistory: (index) -> restoreHistory: (index) ->
@view.editorElement.getModel().setText(@history(index).value) @view.editor.setText(@history(index).value)
history: (index) -> history: (index) ->
@exState.getExHistoryItem(index) @exState.getExHistoryItem(index)
tabAutocomplete: (event) =>
if event.keyCode == 9
event.stopPropagation()
event.preventDefault()
completed = @autoComplete.getAutocomplete(@view.editorElement.getModel().getText())
if completed
@view.editorElement.getModel().setText(completed)
return false
else
@autoComplete.resetCompletion()
increaseHistoryEx: => increaseHistoryEx: =>
if @history(@historyIndex + 1)? if @history(@historyIndex + 1)?
@historyIndex += 1 @historyIndex += 1
@ -45,7 +24,7 @@ class ExViewModel extends ViewModel
if @historyIndex <= 0 if @historyIndex <= 0
# get us back to a clean slate # get us back to a clean slate
@historyIndex = -1 @historyIndex = -1
@view.editorElement.getModel().setText('') @view.editor.setText('')
else else
@historyIndex -= 1 @historyIndex -= 1
@restoreHistory(@historyIndex) @restoreHistory(@historyIndex)

View file

@ -1,472 +1,9 @@
path = require 'path'
CommandError = require './command-error'
fs = require 'fs-plus'
VimOption = require './vim-option'
_ = require 'underscore-plus'
atom
defer = () ->
deferred = {}
deferred.promise = new Promise((resolve, reject) ->
deferred.resolve = resolve
deferred.reject = reject
)
return deferred
trySave = (func) ->
deferred = defer()
try
response = func()
if response instanceof Promise
response.then ->
deferred.resolve()
else
deferred.resolve()
catch error
if error.message.endsWith('is a directory')
atom.notifications.addWarning("Unable to save file: #{error.message}")
else if error.path?
if error.code is 'EACCES'
atom.notifications
.addWarning("Unable to save file: Permission denied '#{error.path}'")
else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST']
atom.notifications.addWarning("Unable to save file '#{error.path}'",
detail: error.message)
else if error.code is 'EROFS'
atom.notifications.addWarning(
"Unable to save file: Read-only file system '#{error.path}'")
else if (errorMatch =
/ENOTDIR, not a directory '([^']+)'/.exec(error.message))
fileName = errorMatch[1]
atom.notifications.addWarning("Unable to save file: A directory in the "+
"path '#{fileName}' could not be written to")
else
throw error
deferred.promise
saveAs = (filePath, editor) ->
fs.writeFileSync(filePath, editor.getText())
getFullPath = (filePath) ->
filePath = fs.normalize(filePath)
if path.isAbsolute(filePath)
filePath
else if atom.project.getPaths().length == 0
path.join(fs.normalize('~'), filePath)
else
path.join(atom.project.getPaths()[0], filePath)
replaceGroups = (groups, string) ->
replaced = ''
escaped = false
while (char = string[0])?
string = string[1..]
if char is '\\' and not escaped
escaped = true
else if /\d/.test(char) and escaped
escaped = false
group = groups[parseInt(char)]
group ?= ''
replaced += group
else
escaped = false
replaced += char
replaced
getSearchTerm = (term, modifiers = {'g': true}) ->
escaped = false
hasc = false
hasC = false
term_ = term
term = ''
for char in term_
if char is '\\' and not escaped
escaped = true
term += char
else
if char is 'c' and escaped
hasc = true
term = term[...-1]
else if char is 'C' and escaped
hasC = true
term = term[...-1]
else if char isnt '\\'
term += char
escaped = false
if hasC
modifiers['i'] = false
if (not hasC and not term.match('[A-Z]') and \
atom.config.get('vim-mode.useSmartcaseForSearch')) or hasc
modifiers['i'] = true
modFlags = Object.keys(modifiers).filter((key) -> modifiers[key]).join('')
try
new RegExp(term, modFlags)
catch
new RegExp(_.escapeRegExp(term), modFlags)
class Ex class Ex
@singleton: => write: ->
@ex ||= new Ex if atom.workspace.getActiveTextEditor().getPath() isnt undefined
atom.workspace.getActiveEditor().save()
@registerCommand: (name, func) =>
@singleton()[name] = func
@registerAlias: (alias, name) =>
@singleton()[alias] = (args) => @singleton()[name](args)
@getCommands: () =>
Object.keys(Ex.singleton()).concat(Object.keys(Ex.prototype)).filter((cmd, index, list) ->
list.indexOf(cmd) == index
)
quit: ->
atom.workspace.getActivePane().destroyActiveItem()
quitall: ->
if !atom.config.get('ex-mode.onlyCloseBuffers')
atom.close()
else else
atom.workspace.getTextEditors().forEach (editor) -> atom.workspace.getActivePane().saveActiveItemAs()
editor.destroy() w: => @write()
q: => @quit()
qall: => @quitall()
tabedit: (args) =>
if args.args.trim() isnt ''
@edit(args)
else
@tabnew(args)
tabe: (args) => @tabedit(args)
tabnew: (args) =>
if args.args.trim() is ''
atom.workspace.open()
else
@tabedit(args)
tabclose: (args) => @quit(args)
tabc: => @tabclose()
tabnext: ->
pane = atom.workspace.getActivePane()
pane.activateNextItem()
tabn: => @tabnext()
tabprevious: ->
pane = atom.workspace.getActivePane()
pane.activatePreviousItem()
tabp: => @tabprevious()
tabonly: ->
tabBar = atom.workspace.getPanes()[0]
tabBarElement = atom.views.getView(tabBar).querySelector(".tab-bar")
tabBarElement.querySelector(".right-clicked") && tabBarElement.querySelector(".right-clicked").classList.remove("right-clicked")
tabBarElement.querySelector(".active").classList.add("right-clicked")
atom.commands.dispatch(tabBarElement, 'tabs:close-other-tabs')
tabBarElement.querySelector(".active").classList.remove("right-clicked")
tabo: => @tabonly()
edit: ({ range, args, editor }) ->
filePath = args.trim()
if filePath[0] is '!'
force = true
filePath = filePath[1..].trim()
else
force = false
if editor.isModified() and not force
throw new CommandError('No write since last change (add ! to override)')
if filePath.indexOf(' ') isnt -1
throw new CommandError('Only one file name allowed')
if filePath.length isnt 0
fullPath = getFullPath(filePath)
if fullPath is editor.getPath()
editor.getBuffer().reload()
else
atom.workspace.open(fullPath)
else
if editor.getPath()?
editor.getBuffer().reload()
else
throw new CommandError('No file name')
e: (args) => @edit(args)
enew: ->
atom.workspace.open()
write: ({ range, args, editor, saveas }) ->
saveas ?= false
filePath = args
if filePath[0] is '!'
force = true
filePath = filePath[1..]
else
force = false
filePath = filePath.trim()
if filePath.indexOf(' ') isnt -1
throw new CommandError('Only one file name allowed')
deferred = defer()
editor = atom.workspace.getActiveTextEditor()
# Case 1; path is provided
if filePath.length isnt 0
fullPath = getFullPath filePath
# Only write when it does not exist or we have a force flag set.
if force or not fs.existsSync(fullPath)
editor.saveAs(fullPath)
return deferred.promise
throw new CommandError("File exists (add ! to override)")
# Case 2; no path provided, call editor save.
editor = atom.workspace.getActiveTextEditor()
# Does the current buffer exist?
if editor.getPath()? and fs.existsSync(editor.getPath())
trySave(-> editor.save()).then(deferred.promise)
else
# Cant see what the better API is but Pane.saveActiveItemAs() is the only call
# I could find that states it will ask the user.
trySave(-> atom.workspace.getActivePane().saveActiveItemAs()).then(deferred.promise)
return deferred.promise
wall: ->
atom.workspace.saveAll()
w: (args) =>
@write(args)
wq: (args) =>
@write(args).then(=> @quit())
wa: =>
@wall()
wqall: =>
@wall()
@quitall()
wqa: =>
@wqall()
xall: =>
@wqall()
xa: =>
@wqall()
saveas: (args) =>
args.saveas = true
@write(args)
xit: (args) => @wq(args)
x: (args) => @xit(args)
split: ({ range, args }) ->
args = args.trim()
filePaths = args.split(' ')
filePaths = undefined if filePaths.length is 1 and filePaths[0] is ''
pane = atom.workspace.getActivePane()
if atom.config.get('ex-mode.splitbelow')
if filePaths? and filePaths.length > 0
newPane = pane.splitDown()
for file in filePaths
do ->
atom.workspace.openURIInPane file, newPane
else
pane.splitDown(copyActiveItem: true)
else
if filePaths? and filePaths.length > 0
newPane = pane.splitUp()
for file in filePaths
do ->
atom.workspace.openURIInPane file, newPane
else
pane.splitUp(copyActiveItem: true)
sp: (args) => @split(args)
substitute: ({ range, args, editor, vimState }) ->
args_ = args.trimLeft()
delim = args_[0]
if /[a-z1-9\\"|]/i.test(delim)
throw new CommandError(
"Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'")
args_ = args_[1..]
escapeChars = {t: '\t', n: '\n', r: '\r'}
parsed = ['', '', '']
parsing = 0
escaped = false
while (char = args_[0])?
args_ = args_[1..]
if char is delim
if not escaped
parsing++
if parsing > 2
throw new CommandError('Trailing characters')
else
parsed[parsing] = parsed[parsing][...-1]
else if char is '\\' and not escaped
parsed[parsing] += char
escaped = true
else if parsing == 1 and escaped and escapeChars[char]?
parsed[parsing] += escapeChars[char]
escaped = false
else
escaped = false
parsed[parsing] += char
[pattern, substition, flags] = parsed
if pattern is ''
if vimState.getSearchHistoryItem?
# vim-mode
pattern = vimState.getSearchHistoryItem()
else if vimState.searchHistory?
# vim-mode-plus
pattern = vimState.searchHistory.get('prev')
if not pattern?
atom.beep()
throw new CommandError('No previous regular expression')
else
if vimState.pushSearchHistory?
# vim-mode
vimState.pushSearchHistory(pattern)
else if vimState.searchHistory?
# vim-mode-plus
vimState.searchHistory.save(pattern)
try
flagsObj = {}
flags.split('').forEach((flag) -> flagsObj[flag] = true)
# gdefault option
if atom.config.get('ex-mode.gdefault')
flagsObj.g = !flagsObj.g
patternRE = getSearchTerm(pattern, flagsObj)
catch e
if e.message.indexOf('Invalid flags supplied to RegExp constructor') is 0
throw new CommandError("Invalid flags: #{e.message[45..]}")
else if e.message.indexOf('Invalid regular expression: ') is 0
throw new CommandError("Invalid RegEx: #{e.message[27..]}")
else
throw e
editor.transact ->
for line in [range[0]..range[1]]
editor.scanInBufferRange(
patternRE,
[[line, 0], [line + 1, 0]],
({match, replace}) ->
replace(replaceGroups(match[..], substition))
)
s: (args) => @substitute(args)
vsplit: ({ range, args }) ->
args = args.trim()
filePaths = args.split(' ')
filePaths = undefined if filePaths.length is 1 and filePaths[0] is ''
pane = atom.workspace.getActivePane()
if atom.config.get('ex-mode.splitright')
if filePaths? and filePaths.length > 0
newPane = pane.splitRight()
for file in filePaths
do ->
atom.workspace.openURIInPane file, newPane
else
pane.splitRight(copyActiveItem: true)
else
if filePaths? and filePaths.length > 0
newPane = pane.splitLeft()
for file in filePaths
do ->
atom.workspace.openURIInPane file, newPane
else
pane.splitLeft(copyActiveItem: true)
vsp: (args) => @vsplit(args)
delete: ({ range }) ->
range = [[range[0], 0], [range[1] + 1, 0]]
editor = atom.workspace.getActiveTextEditor()
text = editor.getTextInBufferRange(range)
atom.clipboard.write(text)
editor.buffer.setTextInRange(range, '')
yank: ({ range }) ->
range = [[range[0], 0], [range[1] + 1, 0]]
txt = atom.workspace.getActiveTextEditor().getTextInBufferRange(range)
atom.clipboard.write(txt);
set: ({ range, args }) ->
args = args.trim()
if args == ""
throw new CommandError("No option specified")
options = args.split(' ')
for option in options
do ->
if option.includes("=")
nameValPair = option.split("=")
if (nameValPair.length != 2)
throw new CommandError("Wrong option format. [name]=[value] format is expected")
optionName = nameValPair[0]
optionValue = nameValPair[1]
optionProcessor = VimOption.singleton()[optionName]
if not optionProcessor?
throw new CommandError("No such option: #{optionName}")
optionProcessor(optionValue)
else
optionProcessor = VimOption.singleton()[option]
if not optionProcessor?
throw new CommandError("No such option: #{option}")
optionProcessor()
sort: ({ range }) =>
editor = atom.workspace.getActiveTextEditor()
sortingRange = [[]]
# If no range is provided, the entire file should be sorted.
isMultiLine = range[1] - range[0] > 1
if isMultiLine
sortingRange = [[range[0], 0], [range[1] + 1, 0]]
else
sortingRange = [[0, 0], [editor.getLastBufferRow(), 0]]
# Store every bufferedRow string in an array.
textLines = []
for lineIndex in [sortingRange[0][0]..sortingRange[1][0] - 1]
textLines.push(editor.lineTextForBufferRow(lineIndex))
# Sort the array and join them together with newlines for writing back to the file.
sortedText = _.sortBy(textLines).join('\n') + '\n'
editor.buffer.setTextInRange(sortingRange, sortedText)
module.exports = Ex module.exports = Ex

View file

@ -1,85 +0,0 @@
_ = require 'underscore-plus'
getSearchTerm = (term, modifiers = {'g': true}) ->
escaped = false
hasc = false
hasC = false
term_ = term
term = ''
for char in term_
if char is '\\' and not escaped
escaped = true
term += char
else
if char is 'c' and escaped
hasc = true
term = term[...-1]
else if char is 'C' and escaped
hasC = true
term = term[...-1]
else if char isnt '\\'
term += char
escaped = false
if hasC
modifiers['i'] = false
if (not hasC and not term.match('[A-Z]') and \
atom.config.get("vim-mode:useSmartcaseForSearch")) or hasc
modifiers['i'] = true
modFlags = Object.keys(modifiers).filter((key) -> modifiers[key]).join('')
try
new RegExp(term, modFlags)
catch
new RegExp(_.escapeRegExp(term), modFlags)
module.exports = {
findInBuffer : (buffer, pattern) ->
found = []
buffer.scan(new RegExp(pattern, 'g'), (obj) -> found.push obj.range)
return found
findNextInBuffer : (buffer, curPos, pattern) ->
found = @findInBuffer(buffer, pattern)
more = (i for i in found when i.compare([curPos, curPos]) is 1)
if more.length > 0
return more[0].start.row
else if found.length > 0
return found[0].start.row
else
return null
findPreviousInBuffer : (buffer, curPos, pattern) ->
found = @findInBuffer(buffer, pattern)
less = (i for i in found when i.compare([curPos, curPos]) is -1)
if less.length > 0
return less[less.length - 1].start.row
else if found.length > 0
return found[found.length - 1].start.row
else
return null
# Returns an array of ranges of all occurences of `term` in `editor`.
# The array is sorted so that the first occurences after the cursor come
# first (and the search wraps around). If `reverse` is true, the array is
# reversed so that the first occurence before the cursor comes first.
scanEditor: (term, editor, position, reverse = false) ->
[rangesBefore, rangesAfter] = [[], []]
editor.scan getSearchTerm(term), ({range}) ->
if reverse
isBefore = range.start.compare(position) < 0
else
isBefore = range.start.compare(position) <= 0
if isBefore
rangesBefore.push(range)
else
rangesAfter.push(range)
if reverse
rangesAfter.concat(rangesBefore).reverse()
else
rangesAfter.concat(rangesBefore)
}

View file

@ -1,5 +1,4 @@
class GlobalExState class GlobalExState
commandHistory: [] commandHistory: []
setVim: (@vim) ->
module.exports = GlobalExState module.exports = GlobalExState

View file

@ -1,22 +1,18 @@
ExNormalModeInputElement = require './ex-normal-mode-input-element' ExCommandModeInputView = require './ex-command-mode-input-view'
class ViewModel class ViewModel
constructor: (@command, opts={}) -> constructor: (@command, opts={}) ->
{@editor, @exState} = @command {@editor, @exState} = @command
@view = new ExNormalModeInputElement().initialize(@, opts) @view = new ExCommandModeInputView(@, opts)
@editor.normalModeInputView = @view @editor.commandModeInputView = @view
@exState.onDidFailToExecute => @view.remove() @exState.onDidFailToExecute => @view.remove()
@done = false
confirm: (view) -> confirm: (view) ->
@exState.pushOperations(new Input(@view.value)) @exState.pushOperations(new Input(@view.value))
@done = true
cancel: (view) -> cancel: (view) ->
unless @done @exState.pushOperations(new Input(''))
@exState.pushOperations(new Input(''))
@done = true
class Input class Input
constructor: (@characters) -> constructor: (@characters) ->

View file

@ -1,65 +0,0 @@
class VimOption
@singleton: =>
@option ||= new VimOption
list: =>
atom.config.set("editor.showInvisibles", true)
nolist: =>
atom.config.set("editor.showInvisibles", false)
number: =>
atom.config.set("editor.showLineNumbers", true)
nu: =>
@number()
nonumber: =>
atom.config.set("editor.showLineNumbers", false)
nonu: =>
@nonumber()
splitright: =>
atom.config.set("ex-mode.splitright", true)
spr: =>
@splitright()
nosplitright: =>
atom.config.set("ex-mode.splitright", false)
nospr: =>
@nosplitright()
splitbelow: =>
atom.config.set("ex-mode.splitbelow", true)
sb: =>
@splitbelow()
nosplitbelow: =>
atom.config.set("ex-mode.splitbelow", false)
nosb: =>
@nosplitbelow()
smartcase: =>
atom.config.set("vim-mode.useSmartcaseForSearch", true)
scs: =>
@smartcase()
nosmartcase: =>
atom.config.set("vim-mode.useSmartcaseForSearch", false)
noscs: =>
@nosmartcase()
gdefault: =>
atom.config.set("ex-mode.gdefault", true)
nogdefault: =>
atom.config.set("ex-mode.gdefault", false)
module.exports = VimOption

View file

@ -1,7 +1,7 @@
{ {
"name": "ex-mode", "name": "ex-mode",
"main": "./lib/ex-mode", "main": "./lib/ex-mode",
"version": "0.18.0", "version": "0.1.1",
"description": "Ex for Atom's vim-mode", "description": "Ex for Atom's vim-mode",
"activationCommands": { "activationCommands": {
"atom-workspace": "ex-mode:open" "atom-workspace": "ex-mode:open"
@ -9,36 +9,10 @@
"repository": "https://github.com/lloeki/ex-mode", "repository": "https://github.com/lloeki/ex-mode",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"atom": ">=0.200.0 <2.0.0" "atom": ">=0.174.0 <2.0.0"
}, },
"dependencies": { "dependencies": {
"underscore-plus": "1.x", "underscore-plus": "1.x",
"event-kit": "^0.7.2", "event-kit": "^0.7.2"
"space-pen": "^5.1.1",
"atom-space-pen-views": "^2.0.4",
"fs-plus": "^2.2.8"
},
"consumedServices": {
"vim-mode": {
"versions": {
"^0.1.0": "consumeVim"
}
},
"vim-mode-plus": {
"versions": {
"^0.1.0": "consumeVimModePlus"
}
}
},
"providedServices": {
"ex-mode": {
"description": "Ex commands",
"versions": {
"0.20.0": "provideEx"
}
}
},
"devDependencies": {
"node-uuid": "^1.4.2"
} }
} }

View file

@ -1,100 +0,0 @@
fs = require 'fs-plus'
path = require 'path'
os = require 'os'
uuid = require 'node-uuid'
helpers = require './spec-helper'
AutoComplete = require '../lib/autocomplete'
describe "autocomplete functionality", ->
beforeEach ->
@autoComplete = new AutoComplete(['taba', 'tabb', 'tabc'])
@testDir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}")
@nonExistentTestDir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}")
@testFile1 = path.join(@testDir, "atom-ex-testfile-a.txt")
@testFile2 = path.join(@testDir, "atom-ex-testfile-b.txt")
runs =>
fs.makeTreeSync(@testDir)
fs.closeSync(fs.openSync(@testFile1, 'w'));
fs.closeSync(fs.openSync(@testFile2, 'w'));
spyOn(@autoComplete, 'resetCompletion').andCallThrough()
spyOn(@autoComplete, 'getFilePathCompletion').andCallThrough()
spyOn(@autoComplete, 'getCommandCompletion').andCallThrough()
afterEach ->
fs.removeSync(@testDir)
describe "autocomplete commands", ->
beforeEach ->
@completed = @autoComplete.getAutocomplete('tab')
it "returns taba", ->
expect(@completed).toEqual('taba')
it "calls command function", ->
expect(@autoComplete.getCommandCompletion.callCount).toBe(1)
describe "autocomplete commands, then autoComplete again", ->
beforeEach ->
@completed = @autoComplete.getAutocomplete('tab')
@completed = @autoComplete.getAutocomplete('tab')
it "returns tabb", ->
expect(@completed).toEqual('tabb')
it "calls command function", ->
expect(@autoComplete.getCommandCompletion.callCount).toBe(1)
describe "autocomplete directory", ->
beforeEach ->
filePath = path.join(os.tmpdir(), 'atom-ex-mode-spec-')
@completed = @autoComplete.getAutocomplete('tabe ' + filePath)
it "returns testDir", ->
expected = 'tabe ' + @testDir + path.sep
expect(@completed).toEqual(expected)
it "clears autocomplete", ->
expect(@autoComplete.resetCompletion.callCount).toBe(1)
describe "autocomplete directory, then autocomplete again", ->
beforeEach ->
filePath = path.join(os.tmpdir(), 'atom-ex-mode-spec-')
@completed = @autoComplete.getAutocomplete('tabe ' + filePath)
@completed = @autoComplete.getAutocomplete(@completed)
it "returns test file 1", ->
expect(@completed).toEqual('tabe ' + @testFile1)
it "lists files twice", ->
expect(@autoComplete.getFilePathCompletion.callCount).toBe(2)
describe "autocomplete full directory, then autocomplete again", ->
beforeEach ->
filePath = path.join(@testDir, 'a')
@completed = @autoComplete.getAutocomplete('tabe ' + filePath)
@completed = @autoComplete.getAutocomplete(@completed)
it "returns test file 2", ->
expect(@completed).toEqual('tabe ' + @testFile2)
it "lists files once", ->
expect(@autoComplete.getFilePathCompletion.callCount).toBe(1)
describe "autocomplete non existent directory", ->
beforeEach ->
@completed = @autoComplete.getAutocomplete('tabe ' + @nonExistentTestDir)
it "returns no completions", ->
expected = '';
expect(@completed).toEqual(expected)
describe "autocomplete existing file as directory", ->
beforeEach ->
filePath = @testFile1 + path.sep
@completed = @autoComplete.getAutocomplete('tabe ' + filePath)
it "returns no completions", ->
expected = '';
expect(@completed).toEqual(expected)

File diff suppressed because it is too large Load diff

View file

@ -1,92 +0,0 @@
helpers = require './spec-helper'
describe "the input element", ->
[editor, editorElement, vimState, exState] = []
beforeEach ->
vimMode = atom.packages.loadPackage('vim-mode-plus')
exMode = atom.packages.loadPackage('ex-mode')
waitsForPromise ->
activationPromise = exMode.activate()
helpers.activateExMode()
activationPromise
runs ->
spyOn(exMode.mainModule.globalExState, 'setVim').andCallThrough()
waitsForPromise ->
vimMode.activate()
waitsFor ->
exMode.mainModule.globalExState.setVim.calls.length > 0
runs ->
helpers.getEditorElement (element) ->
atom.commands.dispatch(element, "ex-mode:open")
editorElement = element
editor = editorElement.getModel()
atom.commands.dispatch(getCommandEditor(), "core:cancel")
vimState = vimMode.mainModule.getEditorState(editor)
exState = exMode.mainModule.exStates.get(editor)
vimState.resetNormalMode()
editor.setText("abc\ndef\nabc\ndef")
afterEach ->
atom.commands.dispatch(getCommandEditor(), "core:cancel")
getVisibility = () ->
editor.normalModeInputView.panel.visible
getCommandEditor = () ->
editor.normalModeInputView.editorElement
it "opens with 'ex-mode:open'", ->
atom.commands.dispatch(editorElement, "ex-mode:open")
expect(getVisibility()).toBe true
it "closes with 'core:cancel'", ->
atom.commands.dispatch(editorElement, "ex-mode:open")
expect(getVisibility()).toBe true
atom.commands.dispatch(getCommandEditor(), "core:cancel")
expect(getVisibility()).toBe false
it "closes when opening and then pressing backspace", ->
atom.commands.dispatch(editorElement, "ex-mode:open")
expect(getVisibility()).toBe true
atom.commands.dispatch(getCommandEditor(), "core:backspace")
expect(getVisibility()).toBe false
it "doesn't close when there is text and pressing backspace", ->
atom.commands.dispatch(editorElement, "ex-mode:open")
expect(getVisibility()).toBe true
commandEditor = getCommandEditor()
model = commandEditor.getModel()
model.setText('abc')
atom.commands.dispatch(commandEditor, "core:backspace")
expect(getVisibility()).toBe true
expect(model.getText()).toBe 'ab'
it "closes when there is text and pressing backspace multiple times", ->
atom.commands.dispatch(editorElement, "ex-mode:open")
expect(getVisibility()).toBe true
commandEditor = getCommandEditor()
model = commandEditor.getModel()
expect(model.getText()).toBe ''
model.setText('abc')
atom.commands.dispatch(commandEditor, "core:backspace")
expect(getVisibility()).toBe true
expect(model.getText()).toBe 'ab'
atom.commands.dispatch(commandEditor, "core:backspace")
expect(getVisibility()).toBe true
expect(model.getText()).toBe 'a'
atom.commands.dispatch(commandEditor, "core:backspace")
expect(getVisibility()).toBe true
expect(model.getText()).toBe ''
atom.commands.dispatch(commandEditor, "core:backspace")
expect(getVisibility()).toBe false
it "contains '<,'> when opened while there are selections", ->
editor.setCursorBufferPosition([0, 0])
editor.selectToBufferPosition([0, 1])
editor.addCursorAtBufferPosition([2, 0])
editor.selectToBufferPosition([2, 1])
atom.commands.dispatch(editorElement, "ex-mode:open")
expect(getCommandEditor().getModel().getText()).toBe "'<,'>"

62
spec/ex-mode-spec.coffee Normal file
View file

@ -0,0 +1,62 @@
ExMode = require '../lib/ex-mode'
# Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs.
#
# To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit`
# or `fdescribe`). Remove the `f` to unfocus the block.
describe "ExMode", ->
[workspaceElement, activationPromise] = []
beforeEach ->
workspaceElement = atom.views.getView(atom.workspace)
activationPromise = atom.packages.activatePackage('ex-mode')
describe "when the ex-mode:toggle event is triggered", ->
it "hides and shows the modal panel", ->
# Before the activation event the view is not on the DOM, and no panel
# has been created
expect(workspaceElement.querySelector('.ex-mode')).not.toExist()
# This is an activation event, triggering it will cause the package to be
# activated.
atom.commands.dispatch workspaceElement, 'ex-mode:toggle'
waitsForPromise ->
activationPromise
runs ->
expect(workspaceElement.querySelector('.ex-mode')).toExist()
exModeElement = workspaceElement.querySelector('.ex-mode')
expect(exModeElement).toExist()
exModePanel = atom.workspace.panelForItem(exModeElement)
expect(exModePanel.isVisible()).toBe true
atom.commands.dispatch workspaceElement, 'ex-mode:toggle'
expect(exModePanel.isVisible()).toBe false
it "hides and shows the view", ->
# This test shows you an integration test testing at the view level.
# Attaching the workspaceElement to the DOM is required to allow the
# `toBeVisible()` matchers to work. Anything testing visibility or focus
# requires that the workspaceElement is on the DOM. Tests that attach the
# workspaceElement to the DOM are generally slower than those off DOM.
jasmine.attachToDOM(workspaceElement)
expect(workspaceElement.querySelector('.ex-mode')).not.toExist()
# This is an activation event, triggering it causes the package to be
# activated.
atom.commands.dispatch workspaceElement, 'ex-mode:toggle'
waitsForPromise ->
activationPromise
runs ->
# Now we can test for view visibility
exModeElement = workspaceElement.querySelector('.ex-mode')
expect(exModeElement).toBeVisible()
atom.commands.dispatch workspaceElement, 'ex-mode:toggle'
expect(exModeElement).not.toBeVisible()

View file

@ -0,0 +1,5 @@
ExModeView = require '../lib/ex-mode-view'
describe "ExModeView", ->
it "has one valid test", ->
expect("life").toBe "easy"

View file

@ -1,65 +0,0 @@
ExState = require '../lib/ex-state'
GlobalExState = require '../lib/global-ex-state'
beforeEach ->
atom.workspace ||= {}
activateExMode = ->
atom.workspace.open().then ->
atom.commands.dispatch(atom.views.getView(atom.workspace), 'ex-mode:open')
keydown('escape')
atom.workspace.getActivePane().destroyActiveItem()
getEditorElement = (callback) ->
textEditor = null
waitsForPromise ->
atom.workspace.open().then (e) ->
textEditor = e
runs ->
# element = document.createElement("atom-text-editor")
# element.setModel(textEditor)
# element.classList.add('vim-mode')
# element.exState = new ExState(element, new GlobalExState)
#
# element.addEventListener "keydown", (e) ->
# atom.keymaps.handleKeyboardEvent(e)
element = atom.views.getView(textEditor)
callback(element)
dispatchKeyboardEvent = (target, eventArgs...) ->
e = document.createEvent('KeyboardEvent')
e.initKeyboardEvent(eventArgs...)
# 0 is the default, and it's valid ASCII, but it's wrong.
Object.defineProperty(e, 'keyCode', get: -> undefined) if e.keyCode is 0
target.dispatchEvent e
dispatchTextEvent = (target, eventArgs...) ->
e = document.createEvent('TextEvent')
e.initTextEvent(eventArgs...)
target.dispatchEvent e
keydown = (key, {element, ctrl, shift, alt, meta, raw}={}) ->
key = "U+#{key.charCodeAt(0).toString(16)}" unless key is 'escape' or raw?
element ||= document.activeElement
eventArgs = [
true, # bubbles
true, # cancelable
null, # view
key, # key
0, # location
ctrl, alt, shift, meta
]
canceled = not dispatchKeyboardEvent(element, 'keydown', eventArgs...)
dispatchKeyboardEvent(element, 'keypress', eventArgs...)
if not canceled
if dispatchTextEvent(element, 'textInput', eventArgs...)
element.value += key
dispatchKeyboardEvent(element, 'keyup', eventArgs...)
module.exports = {keydown, getEditorElement, activateExMode}

View file

@ -6,25 +6,3 @@
.ex-mode { .ex-mode {
} }
div[is=ex-command-mode-input] atom-text-editor[mini]::before {
content: ":";
opacity: 0.5;
}
div[is=vim-normal-mode-input] atom-text-editor[mini]::before {
content: "/";
opacity: 0.5;
}
.command-mode-input atom-text-editor[mini] {
background-color: inherit;
border: none;
width: 100%;
font-weight: normal;
color: @text-color;
line-height: 1.28;
cursor: default;
white-space: nowrap;
padding-left: 10px;
}