Merge pull request #83 from jazzpi/specs
Add specs; minor changes to some commands
This commit is contained in:
commit
edea63a575
9 changed files with 670 additions and 132 deletions
|
|
@ -34,6 +34,9 @@ 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
|
||||||
|
|
||||||
|
|
@ -55,5 +58,6 @@ class ExState
|
||||||
else
|
else
|
||||||
throw e
|
throw e
|
||||||
@clearOpStack()
|
@clearOpStack()
|
||||||
|
@emitter.emit('processed-op-stack')
|
||||||
|
|
||||||
module.exports = ExState
|
module.exports = ExState
|
||||||
|
|
|
||||||
139
lib/ex.coffee
139
lib/ex.coffee
|
|
@ -37,31 +37,32 @@ saveAs = (filePath) ->
|
||||||
fs.writeFileSync(filePath, editor.getText())
|
fs.writeFileSync(filePath, editor.getText())
|
||||||
|
|
||||||
getFullPath = (filePath) ->
|
getFullPath = (filePath) ->
|
||||||
|
filePath = fs.normalize(filePath)
|
||||||
|
|
||||||
if path.isAbsolute(filePath)
|
if path.isAbsolute(filePath)
|
||||||
fullPath = filePath
|
filePath
|
||||||
else if atom.project.getPaths().length == 0
|
else if atom.project.getPaths().length == 0
|
||||||
fullPath = path.join('~', filePath)
|
path.join(fs.normalize('~'), filePath)
|
||||||
else
|
else
|
||||||
fullPath = path.join(atom.project.getPaths()[0], filePath)
|
path.join(atom.project.getPaths()[0], filePath)
|
||||||
|
|
||||||
return fs.normalize(fullPath)
|
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
|
||||||
|
|
||||||
replaceGroups = (groups, replString) ->
|
replaced
|
||||||
arr = replString.split('')
|
|
||||||
offset = 0
|
|
||||||
cdiff = 0
|
|
||||||
|
|
||||||
while (m = replString.match(/(?:[^\\]|^)\\(\d)/))?
|
|
||||||
group = groups[m[1]] or ''
|
|
||||||
i = replString.indexOf(m[0])
|
|
||||||
l = m[0].length
|
|
||||||
replString = replString.slice(i + l)
|
|
||||||
arr[i + offset...i + offset + l] = (if l is 2 then '' else m[0][0]) +
|
|
||||||
group
|
|
||||||
arr = arr.join('').split ''
|
|
||||||
offset += i + l - group.length
|
|
||||||
|
|
||||||
return arr.join('').replace(/\\\\(\d)/, '\\$1')
|
|
||||||
|
|
||||||
class Ex
|
class Ex
|
||||||
@singleton: =>
|
@singleton: =>
|
||||||
|
|
@ -75,21 +76,21 @@ class Ex
|
||||||
|
|
||||||
q: => @quit()
|
q: => @quit()
|
||||||
|
|
||||||
tabedit: (range, args) ->
|
tabedit: (range, args) =>
|
||||||
args = args.trim()
|
if args.trim() isnt ''
|
||||||
filePaths = args.split(' ')
|
@edit(range, args)
|
||||||
pane = atom.workspace.getActivePane()
|
|
||||||
if filePaths? and filePaths.length > 0
|
|
||||||
for file in filePaths
|
|
||||||
do -> atom.workspace.openURIInPane file, pane
|
|
||||||
else
|
else
|
||||||
atom.workspace.openURIInPane('', pane)
|
@tabnew(range, args)
|
||||||
|
|
||||||
tabe: (args...) => @tabedit(args...)
|
tabe: (args...) => @tabedit(args...)
|
||||||
|
|
||||||
tabnew: (args...) => @tabedit(args...)
|
tabnew: (range, args) =>
|
||||||
|
if args.trim() is ''
|
||||||
|
atom.workspace.open()
|
||||||
|
else
|
||||||
|
@tabedit(range, args)
|
||||||
|
|
||||||
tabclose: => @quit()
|
tabclose: (args...) => @quit(args...)
|
||||||
|
|
||||||
tabc: => @tabclose()
|
tabc: => @tabclose()
|
||||||
|
|
||||||
|
|
@ -106,16 +107,30 @@ class Ex
|
||||||
tabp: => @tabprevious()
|
tabp: => @tabprevious()
|
||||||
|
|
||||||
edit: (range, filePath) ->
|
edit: (range, filePath) ->
|
||||||
filePath = fs.normalize(filePath.trim())
|
filePath = filePath.trim()
|
||||||
|
if filePath[0] is '!'
|
||||||
|
force = true
|
||||||
|
filePath = filePath[1..].trim()
|
||||||
|
else
|
||||||
|
force = false
|
||||||
|
|
||||||
|
editor = atom.workspace.getActiveTextEditor()
|
||||||
|
if editor.isModified() and not force
|
||||||
|
throw new CommandError('No write since last change (add ! to override)')
|
||||||
if filePath.indexOf(' ') isnt -1
|
if filePath.indexOf(' ') isnt -1
|
||||||
throw new CommandError('Only one file name allowed')
|
throw new CommandError('Only one file name allowed')
|
||||||
buffer = atom.workspace.getActiveTextEditor().buffer
|
|
||||||
if buffer.isModified()
|
if filePath.length isnt 0
|
||||||
throw new CommandError('Unsaved file')
|
fullPath = getFullPath(filePath)
|
||||||
if filePath is ''
|
if fullPath is editor.getPath()
|
||||||
filePath = buffer.getPath()
|
editor.getBuffer().reload()
|
||||||
buffer.setPath(getFullPath(filePath))
|
else
|
||||||
buffer.load()
|
atom.workspace.open(fullPath)
|
||||||
|
else
|
||||||
|
if editor.getPath()?
|
||||||
|
editor.getBuffer().reload()
|
||||||
|
else
|
||||||
|
throw new CommandError('No file name')
|
||||||
|
|
||||||
e: (args...) => @edit(args...)
|
e: (args...) => @edit(args...)
|
||||||
|
|
||||||
|
|
@ -125,33 +140,33 @@ class Ex
|
||||||
buffer.load()
|
buffer.load()
|
||||||
|
|
||||||
write: (range, filePath) ->
|
write: (range, filePath) ->
|
||||||
|
if filePath[0] is '!'
|
||||||
|
force = true
|
||||||
|
filePath = filePath[1..]
|
||||||
|
else
|
||||||
|
force = false
|
||||||
|
|
||||||
filePath = filePath.trim()
|
filePath = filePath.trim()
|
||||||
|
if filePath.indexOf(' ') isnt -1
|
||||||
|
throw new CommandError('Only one file name allowed')
|
||||||
|
|
||||||
deferred = Promise.defer()
|
deferred = Promise.defer()
|
||||||
|
|
||||||
pane = atom.workspace.getActivePane()
|
|
||||||
editor = atom.workspace.getActiveTextEditor()
|
editor = atom.workspace.getActiveTextEditor()
|
||||||
if editor.getPath()?
|
saved = false
|
||||||
if filePath.length > 0
|
if filePath.length isnt 0
|
||||||
editorPath = editor.getPath()
|
fullPath = getFullPath(filePath)
|
||||||
fullPath = getFullPath(filePath)
|
if editor.getPath()? and (not fullPath? or editor.getPath() == fullPath)
|
||||||
trySave(-> saveAs(fullPath))
|
# Use editor.save when no path is given or the path to the file is given
|
||||||
.then editor.buffer.setPath(editorPath)
|
trySave(-> editor.save()).then(deferred.resolve)
|
||||||
.then deferred.resolve
|
saved = true
|
||||||
else
|
else if not fullPath?
|
||||||
trySave(-> editor.save())
|
fullPath = atom.showSaveDialogSync()
|
||||||
.then deferred.resolve
|
|
||||||
else
|
if not saved and fullPath?
|
||||||
if filePath.length > 0
|
if not force and fs.existsSync(fullPath)
|
||||||
fullPath = getFullPath(filePath)
|
throw new CommandError("File exists (add ! to override)")
|
||||||
trySave(-> saveAs(fullPath))
|
trySave(-> saveAs(fullPath)).then(deferred.resolve)
|
||||||
.then -> editor.buffer.setPath(fullPath)
|
|
||||||
.then deferred.resolve
|
|
||||||
else
|
|
||||||
fullPath = atom.showSaveDialogSync()
|
|
||||||
if fullPath?
|
|
||||||
trySave(-> editor.saveAs(fullPath))
|
|
||||||
.then -> editor.buffer.setPath(fullPath)
|
|
||||||
.then deferred.resolve
|
|
||||||
|
|
||||||
deferred.promise
|
deferred.promise
|
||||||
|
|
||||||
|
|
@ -161,7 +176,7 @@ class Ex
|
||||||
wq: (args...) =>
|
wq: (args...) =>
|
||||||
@write(args...).then => @quit()
|
@write(args...).then => @quit()
|
||||||
|
|
||||||
x: (args...) => @wq(args...)
|
xit: (args...) => @wq(args...)
|
||||||
|
|
||||||
wa: ->
|
wa: ->
|
||||||
atom.workspace.saveAll()
|
atom.workspace.saveAll()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
ExCommandModeInputElement = require './ex-command-mode-input-element'
|
ExNormalModeInputElement = require './ex-normal-mode-input-element'
|
||||||
|
|
||||||
class ViewModel
|
class ViewModel
|
||||||
constructor: (@command, opts={}) ->
|
constructor: (@command, opts={}) ->
|
||||||
{@editor, @exState} = @command
|
{@editor, @exState} = @command
|
||||||
|
|
||||||
@view = new ExCommandModeInputElement().initialize(@, opts)
|
@view = new ExNormalModeInputElement().initialize(@, opts)
|
||||||
@editor.commandModeInputView = @view
|
@editor.normalModeInputView = @view
|
||||||
@exState.onDidFailToExecute => @view.remove()
|
@exState.onDidFailToExecute => @view.remove()
|
||||||
@done = false
|
@done = false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,5 +32,8 @@
|
||||||
"0.20.0": "provideEx"
|
"0.20.0": "provideEx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"node-uuid": "^1.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
518
spec/ex-commands-spec.coffee
Normal file
518
spec/ex-commands-spec.coffee
Normal file
|
|
@ -0,0 +1,518 @@
|
||||||
|
fs = require 'fs-plus'
|
||||||
|
path = require 'path'
|
||||||
|
os = require 'os'
|
||||||
|
uuid = require 'node-uuid'
|
||||||
|
helpers = require './spec-helper'
|
||||||
|
|
||||||
|
Ex = require('../lib/ex').singleton()
|
||||||
|
|
||||||
|
describe "the commands", ->
|
||||||
|
[editor, editorElement, vimState, exState, dir, dir2] = []
|
||||||
|
projectPath = (fileName) -> path.join(dir, fileName)
|
||||||
|
beforeEach ->
|
||||||
|
vimMode = atom.packages.loadPackage('vim-mode')
|
||||||
|
exMode = atom.packages.loadPackage('ex-mode')
|
||||||
|
exMode.activate()
|
||||||
|
|
||||||
|
waitsForPromise ->
|
||||||
|
vimMode.activate().then ->
|
||||||
|
helpers.activateExMode().then ->
|
||||||
|
dir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}")
|
||||||
|
dir2 = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}")
|
||||||
|
fs.makeTreeSync(dir)
|
||||||
|
fs.makeTreeSync(dir2)
|
||||||
|
atom.project.setPaths([dir, dir2])
|
||||||
|
|
||||||
|
helpers.getEditorElement (element) ->
|
||||||
|
atom.commands.dispatch(element, 'ex-mode:open')
|
||||||
|
keydown('escape')
|
||||||
|
editorElement = element
|
||||||
|
editor = editorElement.getModel()
|
||||||
|
vimState = vimMode.mainModule.getEditorState(editor)
|
||||||
|
exState = exMode.mainModule.exStates.get(editor)
|
||||||
|
vimState.activateNormalMode()
|
||||||
|
vimState.resetNormalMode()
|
||||||
|
editor.setText("abc\ndef\nabc\ndef")
|
||||||
|
|
||||||
|
afterEach ->
|
||||||
|
fs.removeSync(dir)
|
||||||
|
fs.removeSync(dir2)
|
||||||
|
|
||||||
|
keydown = (key, options={}) ->
|
||||||
|
options.element ?= editorElement
|
||||||
|
helpers.keydown(key, options)
|
||||||
|
|
||||||
|
normalModeInputKeydown = (key, opts = {}) ->
|
||||||
|
editor.normalModeInputView.editorElement.getModel().setText(key)
|
||||||
|
|
||||||
|
submitNormalModeInputText = (text) ->
|
||||||
|
commandEditor = editor.normalModeInputView.editorElement
|
||||||
|
commandEditor.getModel().setText(text)
|
||||||
|
atom.commands.dispatch(commandEditor, "core:confirm")
|
||||||
|
|
||||||
|
describe ":write", ->
|
||||||
|
describe "when editing a new file", ->
|
||||||
|
beforeEach ->
|
||||||
|
editor.getBuffer().setText('abc\ndef')
|
||||||
|
|
||||||
|
it "opens the save dialog", ->
|
||||||
|
spyOn(atom, 'showSaveDialogSync')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('write')
|
||||||
|
expect(atom.showSaveDialogSync).toHaveBeenCalled()
|
||||||
|
|
||||||
|
it "saves when a path is specified in the save dialog", ->
|
||||||
|
filePath = projectPath('write-from-save-dialog')
|
||||||
|
spyOn(atom, 'showSaveDialogSync').andReturn(filePath)
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('write')
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true)
|
||||||
|
expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc\ndef')
|
||||||
|
|
||||||
|
it "saves when a path is specified in the save dialog", ->
|
||||||
|
spyOn(atom, 'showSaveDialogSync').andReturn(undefined)
|
||||||
|
spyOn(fs, 'writeFileSync')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('write')
|
||||||
|
expect(fs.writeFileSync.calls.length).toBe(0)
|
||||||
|
|
||||||
|
describe "when editing an existing file", ->
|
||||||
|
filePath = ''
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
i++
|
||||||
|
filePath = projectPath("write-#{i}")
|
||||||
|
editor.setText('abc\ndef')
|
||||||
|
editor.saveAs(filePath)
|
||||||
|
|
||||||
|
it "saves the file", ->
|
||||||
|
editor.setText('abc')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('write')
|
||||||
|
expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc')
|
||||||
|
expect(editor.isModified()).toBe(false)
|
||||||
|
|
||||||
|
describe "with a specified path", ->
|
||||||
|
newPath = ''
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
newPath = path.relative(dir, "#{filePath}.new")
|
||||||
|
editor.getBuffer().setText('abc')
|
||||||
|
keydown(':')
|
||||||
|
|
||||||
|
afterEach ->
|
||||||
|
submitNormalModeInputText("write #{newPath}")
|
||||||
|
newPath = path.resolve(dir, fs.normalize(newPath))
|
||||||
|
expect(fs.existsSync(newPath)).toBe(true)
|
||||||
|
expect(fs.readFileSync(newPath, 'utf-8')).toEqual('abc')
|
||||||
|
expect(editor.isModified()).toBe(true)
|
||||||
|
fs.removeSync(newPath)
|
||||||
|
|
||||||
|
it "saves to the path", ->
|
||||||
|
|
||||||
|
it "expands .", ->
|
||||||
|
newPath = path.join('.', newPath)
|
||||||
|
|
||||||
|
it "expands ..", ->
|
||||||
|
newPath = path.join('..', newPath)
|
||||||
|
|
||||||
|
it "expands ~", ->
|
||||||
|
newPath = path.join('~', newPath)
|
||||||
|
|
||||||
|
it "throws an error with more than one path", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('write path1 path2')
|
||||||
|
expect(atom.notifications.notifications[0].message).toEqual(
|
||||||
|
'Command error: Only one file name allowed'
|
||||||
|
)
|
||||||
|
|
||||||
|
describe "when the file already exists", ->
|
||||||
|
existsPath = ''
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
existsPath = projectPath('write-exists')
|
||||||
|
fs.writeFileSync(existsPath, 'abc')
|
||||||
|
|
||||||
|
afterEach ->
|
||||||
|
fs.removeSync(existsPath)
|
||||||
|
|
||||||
|
it "throws an error if the file already exists", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText("write #{existsPath}")
|
||||||
|
expect(atom.notifications.notifications[0].message).toEqual(
|
||||||
|
'Command error: File exists (add ! to override)'
|
||||||
|
)
|
||||||
|
expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc')
|
||||||
|
|
||||||
|
it "writes if forced with :write!", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText("write! #{existsPath}")
|
||||||
|
expect(atom.notifications.notifications).toEqual([])
|
||||||
|
expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef')
|
||||||
|
|
||||||
|
describe ":quit", ->
|
||||||
|
pane = null
|
||||||
|
beforeEach ->
|
||||||
|
waitsForPromise ->
|
||||||
|
pane = atom.workspace.getActivePane()
|
||||||
|
spyOn(pane, 'destroyActiveItem').andCallThrough()
|
||||||
|
atom.workspace.open()
|
||||||
|
|
||||||
|
it "closes the active pane item if not modified", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('quit')
|
||||||
|
expect(pane.destroyActiveItem).toHaveBeenCalled()
|
||||||
|
expect(pane.getItems().length).toBe(1)
|
||||||
|
|
||||||
|
describe "when the active pane item is modified", ->
|
||||||
|
beforeEach ->
|
||||||
|
editor.getBuffer().setText('def')
|
||||||
|
|
||||||
|
it "opens the prompt to save", ->
|
||||||
|
spyOn(pane, 'promptToSaveItem')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('quit')
|
||||||
|
expect(pane.promptToSaveItem).toHaveBeenCalled()
|
||||||
|
|
||||||
|
describe ":tabclose", ->
|
||||||
|
it "acts as an alias to :quit", ->
|
||||||
|
spyOn(Ex, 'tabclose').andCallThrough()
|
||||||
|
spyOn(Ex, 'quit').andCallThrough()
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('tabclose')
|
||||||
|
expect(Ex.quit).toHaveBeenCalledWith(Ex.tabclose.calls[0].args...)
|
||||||
|
|
||||||
|
describe ":tabnext", ->
|
||||||
|
pane = null
|
||||||
|
beforeEach ->
|
||||||
|
waitsForPromise ->
|
||||||
|
pane = atom.workspace.getActivePane()
|
||||||
|
atom.workspace.open().then -> atom.workspace.open()
|
||||||
|
.then -> atom.workspace.open()
|
||||||
|
|
||||||
|
it "switches to the next tab", ->
|
||||||
|
pane.activateItemAtIndex(1)
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('tabnext')
|
||||||
|
expect(pane.getActiveItemIndex()).toBe(2)
|
||||||
|
|
||||||
|
it "wraps around", ->
|
||||||
|
pane.activateItemAtIndex(pane.getItems().length - 1)
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('tabnext')
|
||||||
|
expect(pane.getActiveItemIndex()).toBe(0)
|
||||||
|
|
||||||
|
describe ":tabprevious", ->
|
||||||
|
pane = null
|
||||||
|
beforeEach ->
|
||||||
|
waitsForPromise ->
|
||||||
|
pane = atom.workspace.getActivePane()
|
||||||
|
atom.workspace.open().then -> atom.workspace.open()
|
||||||
|
.then -> atom.workspace.open()
|
||||||
|
|
||||||
|
it "switches to the previous tab", ->
|
||||||
|
pane.activateItemAtIndex(1)
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('tabprevious')
|
||||||
|
expect(pane.getActiveItemIndex()).toBe(0)
|
||||||
|
|
||||||
|
it "wraps around", ->
|
||||||
|
pane.activateItemAtIndex(0)
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('tabprevious')
|
||||||
|
expect(pane.getActiveItemIndex()).toBe(pane.getItems().length - 1)
|
||||||
|
|
||||||
|
describe ":wq", ->
|
||||||
|
beforeEach ->
|
||||||
|
spyOn(Ex, 'write').andCallThrough()
|
||||||
|
spyOn(Ex, 'quit')
|
||||||
|
|
||||||
|
it "writes the file, then quits", ->
|
||||||
|
spyOn(atom, 'showSaveDialogSync').andReturn(projectPath('wq-1'))
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('wq')
|
||||||
|
expect(Ex.write).toHaveBeenCalled()
|
||||||
|
# Since `:wq` only calls `:quit` after `:write` is finished, we need to
|
||||||
|
# wait a bit for the `:quit` call to occur
|
||||||
|
waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100)
|
||||||
|
|
||||||
|
it "doesn't quit when the file is new and no path is specified in the save dialog", ->
|
||||||
|
spyOn(atom, 'showSaveDialogSync').andReturn(undefined)
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('wq')
|
||||||
|
expect(Ex.write).toHaveBeenCalled()
|
||||||
|
wasNotCalled = false
|
||||||
|
# FIXME: This seems dangerous, but setTimeout somehow doesn't work.
|
||||||
|
setImmediate((->
|
||||||
|
wasNotCalled = not Ex.quit.wasCalled))
|
||||||
|
waitsFor((-> wasNotCalled), 100)
|
||||||
|
|
||||||
|
it "passes the file name", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('wq wq-2')
|
||||||
|
expect(Ex.write)
|
||||||
|
.toHaveBeenCalled()
|
||||||
|
expect(Ex.write.calls[0].args[1].trim()).toEqual('wq-2')
|
||||||
|
waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100)
|
||||||
|
|
||||||
|
describe ":xit", ->
|
||||||
|
it "acts as an alias to :wq", ->
|
||||||
|
spyOn(Ex, 'wq')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('xit')
|
||||||
|
expect(Ex.wq).toHaveBeenCalled()
|
||||||
|
|
||||||
|
describe ":edit", ->
|
||||||
|
describe "without a file name", ->
|
||||||
|
it "reloads the file from the disk", ->
|
||||||
|
filePath = projectPath("edit-1")
|
||||||
|
editor.getBuffer().setText('abc')
|
||||||
|
editor.saveAs(filePath)
|
||||||
|
fs.writeFileSync(filePath, 'def')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('edit')
|
||||||
|
# Reloading takes a bit
|
||||||
|
waitsFor((-> editor.getText() is 'def'),
|
||||||
|
"the editor's content to change", 100)
|
||||||
|
|
||||||
|
it "doesn't reload when the file has been modified", ->
|
||||||
|
filePath = projectPath("edit-2")
|
||||||
|
editor.getBuffer().setText('abc')
|
||||||
|
editor.saveAs(filePath)
|
||||||
|
editor.getBuffer().setText('abcd')
|
||||||
|
fs.writeFileSync(filePath, 'def')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('edit')
|
||||||
|
expect(atom.notifications.notifications[0].message).toEqual(
|
||||||
|
'Command error: No write since last change (add ! to override)')
|
||||||
|
isntDef = false
|
||||||
|
setImmediate(-> isntDef = editor.getText() isnt 'def')
|
||||||
|
waitsFor((-> isntDef), "the editor's content not to change", 50)
|
||||||
|
|
||||||
|
it "reloads when the file has been modified and it is forced", ->
|
||||||
|
filePath = projectPath("edit-3")
|
||||||
|
editor.getBuffer().setText('abc')
|
||||||
|
editor.saveAs(filePath)
|
||||||
|
editor.getBuffer().setText('abcd')
|
||||||
|
fs.writeFileSync(filePath, 'def')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('edit!')
|
||||||
|
expect(atom.notifications.notifications.length).toBe(0)
|
||||||
|
waitsFor((-> editor.getText() is 'def')
|
||||||
|
"the editor's content to change", 50)
|
||||||
|
|
||||||
|
it "throws an error when editing a new file", ->
|
||||||
|
editor.getBuffer().reload()
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('edit')
|
||||||
|
expect(atom.notifications.notifications[0].message).toEqual(
|
||||||
|
'Command error: No file name')
|
||||||
|
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||||
|
submitNormalModeInputText('edit!')
|
||||||
|
expect(atom.notifications.notifications[1].message).toEqual(
|
||||||
|
'Command error: No file name')
|
||||||
|
|
||||||
|
describe "with a file name", ->
|
||||||
|
beforeEach ->
|
||||||
|
spyOn(atom.workspace, 'open')
|
||||||
|
editor.getBuffer().reload()
|
||||||
|
|
||||||
|
it "opens the specified path", ->
|
||||||
|
filePath = projectPath('edit-new-test')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText("edit #{filePath}")
|
||||||
|
expect(atom.workspace.open).toHaveBeenCalledWith(filePath)
|
||||||
|
|
||||||
|
it "opens a relative path", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('edit edit-relative-test')
|
||||||
|
expect(atom.workspace.open).toHaveBeenCalledWith(
|
||||||
|
projectPath('edit-relative-test'))
|
||||||
|
|
||||||
|
it "throws an error if trying to open more than one file", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('edit edit-new-test-1 edit-new-test-2')
|
||||||
|
expect(atom.workspace.open.callCount).toBe(0)
|
||||||
|
expect(atom.notifications.notifications[0].message).toEqual(
|
||||||
|
'Command error: Only one file name allowed')
|
||||||
|
|
||||||
|
describe ":tabedit", ->
|
||||||
|
it "acts as an alias to :edit if supplied with a path", ->
|
||||||
|
spyOn(Ex, 'tabedit').andCallThrough()
|
||||||
|
spyOn(Ex, 'edit')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('tabedit tabedit-test')
|
||||||
|
expect(Ex.edit).toHaveBeenCalledWith(Ex.tabedit.calls[0].args...)
|
||||||
|
|
||||||
|
it "acts as an alias to :tabnew if not supplied with a path", ->
|
||||||
|
spyOn(Ex, 'tabedit').andCallThrough()
|
||||||
|
spyOn(Ex, 'tabnew')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('tabedit ')
|
||||||
|
expect(Ex.tabnew)
|
||||||
|
.toHaveBeenCalledWith(Ex.tabedit.calls[0].args...)
|
||||||
|
|
||||||
|
describe ":tabnew", ->
|
||||||
|
it "opens a new tab", ->
|
||||||
|
spyOn(atom.workspace, 'open')
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('tabnew')
|
||||||
|
expect(atom.workspace.open).toHaveBeenCalled()
|
||||||
|
|
||||||
|
describe ":split", ->
|
||||||
|
it "splits the current file upwards", ->
|
||||||
|
pane = atom.workspace.getActivePane()
|
||||||
|
spyOn(pane, 'splitUp').andCallThrough()
|
||||||
|
filePath = projectPath('split')
|
||||||
|
editor.saveAs(filePath)
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('split')
|
||||||
|
expect(pane.splitUp).toHaveBeenCalled()
|
||||||
|
# FIXME: Should test whether the new pane contains a TextEditor
|
||||||
|
# pointing to the same path
|
||||||
|
|
||||||
|
describe ":vsplit", ->
|
||||||
|
it "splits the current file to the left", ->
|
||||||
|
pane = atom.workspace.getActivePane()
|
||||||
|
spyOn(pane, 'splitLeft').andCallThrough()
|
||||||
|
filePath = projectPath('vsplit')
|
||||||
|
editor.saveAs(filePath)
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('vsplit')
|
||||||
|
expect(pane.splitLeft).toHaveBeenCalled()
|
||||||
|
# FIXME: Should test whether the new pane contains a TextEditor
|
||||||
|
# pointing to the same path
|
||||||
|
|
||||||
|
describe ":delete", ->
|
||||||
|
beforeEach ->
|
||||||
|
editor.setText('abc\ndef\nghi\njkl')
|
||||||
|
editor.setCursorBufferPosition([2, 0])
|
||||||
|
|
||||||
|
it "deletes the current line", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('delete')
|
||||||
|
expect(editor.getText()).toEqual('abc\ndef\njkl')
|
||||||
|
|
||||||
|
it "deletes the lines in the given range", ->
|
||||||
|
processedOpStack = false
|
||||||
|
exState.onDidProcessOpStack -> processedOpStack = true
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('1,2delete')
|
||||||
|
expect(editor.getText()).toEqual('ghi\njkl')
|
||||||
|
|
||||||
|
waitsFor -> processedOpStack
|
||||||
|
editor.setText('abc\ndef\nghi\njkl')
|
||||||
|
editor.setCursorBufferPosition([1, 1])
|
||||||
|
# For some reason, keydown(':') doesn't work here :/
|
||||||
|
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||||
|
submitNormalModeInputText(',/k/delete')
|
||||||
|
expect(editor.getText()).toEqual('abc\n')
|
||||||
|
|
||||||
|
it "undos deleting several lines at once", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText('-1,.delete')
|
||||||
|
expect(editor.getText()).toEqual('abc\njkl')
|
||||||
|
atom.commands.dispatch(editorElement, 'core:undo')
|
||||||
|
expect(editor.getText()).toEqual('abc\ndef\nghi\njkl')
|
||||||
|
|
||||||
|
describe ":substitute", ->
|
||||||
|
beforeEach ->
|
||||||
|
editor.setText('abcaABC\ndefdDEF\nabcaABC')
|
||||||
|
editor.setCursorBufferPosition([0, 0])
|
||||||
|
|
||||||
|
it "replaces a character on the current line", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':substitute /a/x')
|
||||||
|
expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC')
|
||||||
|
|
||||||
|
it "doesn't need a space before the arguments", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':substitute/a/x')
|
||||||
|
expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC')
|
||||||
|
|
||||||
|
it "respects modifiers passed to it", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':substitute/a/x/g')
|
||||||
|
expect(editor.getText()).toEqual('xbcxABC\ndefdDEF\nabcaABC')
|
||||||
|
|
||||||
|
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||||
|
submitNormalModeInputText(':substitute/a/x/gi')
|
||||||
|
expect(editor.getText()).toEqual('xbcxxBC\ndefdDEF\nabcaABC')
|
||||||
|
|
||||||
|
it "replaces on multiple lines", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':%substitute/abc/ghi')
|
||||||
|
expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nghiaABC')
|
||||||
|
|
||||||
|
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||||
|
submitNormalModeInputText(':%substitute/abc/ghi/ig')
|
||||||
|
expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nghiaghi')
|
||||||
|
|
||||||
|
it "can't be delimited by letters", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':substitute nanxngi')
|
||||||
|
expect(atom.notifications.notifications[0].message).toEqual(
|
||||||
|
"Command error: Regular expressions can't be delimited by letters")
|
||||||
|
expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC')
|
||||||
|
|
||||||
|
describe "capturing groups", ->
|
||||||
|
beforeEach ->
|
||||||
|
editor.setText('abcaABC\ndefdDEF\nabcaABC')
|
||||||
|
|
||||||
|
it "replaces \\1 with the first group", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':substitute/bc(.{2})/X\\1X')
|
||||||
|
expect(editor.getText()).toEqual('aXaAXBC\ndefdDEF\nabcaABC')
|
||||||
|
|
||||||
|
it "replaces multiple groups", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':substitute/a([a-z]*)aA([A-Z]*)/X\\1XY\\2Y')
|
||||||
|
expect(editor.getText()).toEqual('XbcXYBCY\ndefdDEF\nabcaABC')
|
||||||
|
|
||||||
|
it "replaces \\0 with the entire match", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':substitute/ab(ca)AB/X\\0X')
|
||||||
|
expect(editor.getText()).toEqual('XabcaABXC\ndefdDEF\nabcaABC')
|
||||||
|
|
||||||
|
describe ":set", ->
|
||||||
|
it "throws an error without a specified option", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':set')
|
||||||
|
expect(atom.notifications.notifications[0].message).toEqual(
|
||||||
|
'Command error: No option specified')
|
||||||
|
|
||||||
|
it "sets multiple options at once", ->
|
||||||
|
atom.config.set('editor.showInvisibles', false)
|
||||||
|
atom.config.set('editor.showLineNumbers', false)
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':set list number')
|
||||||
|
expect(atom.config.get('editor.showInvisibles')).toBe(true)
|
||||||
|
expect(atom.config.get('editor.showLineNumbers')).toBe(true)
|
||||||
|
|
||||||
|
describe "the options", ->
|
||||||
|
beforeEach ->
|
||||||
|
atom.config.set('editor.showInvisibles', false)
|
||||||
|
atom.config.set('editor.showLineNumbers', false)
|
||||||
|
|
||||||
|
it "sets (no)list", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':set list')
|
||||||
|
expect(atom.config.get('editor.showInvisibles')).toBe(true)
|
||||||
|
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||||
|
submitNormalModeInputText(':set nolist')
|
||||||
|
expect(atom.config.get('editor.showInvisibles')).toBe(false)
|
||||||
|
|
||||||
|
it "sets (no)nu(mber)", ->
|
||||||
|
keydown(':')
|
||||||
|
submitNormalModeInputText(':set nu')
|
||||||
|
expect(atom.config.get('editor.showLineNumbers')).toBe(true)
|
||||||
|
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||||
|
submitNormalModeInputText(':set nonu')
|
||||||
|
expect(atom.config.get('editor.showLineNumbers')).toBe(false)
|
||||||
|
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||||
|
submitNormalModeInputText(':set number')
|
||||||
|
expect(atom.config.get('editor.showLineNumbers')).toBe(true)
|
||||||
|
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||||
|
submitNormalModeInputText(':set nonumber')
|
||||||
|
expect(atom.config.get('editor.showLineNumbers')).toBe(false)
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
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()
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
ExModeView = require '../lib/ex-mode-view'
|
|
||||||
|
|
||||||
describe "ExModeView", ->
|
|
||||||
it "has one valid test", ->
|
|
||||||
expect("life").toBe "easy"
|
|
||||||
65
spec/spec-helper.coffee
Normal file
65
spec/spec-helper.coffee
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
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}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue