Merge 91f3f82730 into 728ccaa5f9
This commit is contained in:
commit
7f92b90e61
10 changed files with 862 additions and 400 deletions
|
|
@ -14,8 +14,11 @@ Use the service to register commands, from your own package, or straight from `i
|
|||
# in Atom's init.coffee
|
||||
atom.packages.onDidActivatePackage (pack) ->
|
||||
if pack.name == 'ex-mode'
|
||||
Ex = pack.mainModule.provideEx()
|
||||
Ex.registerCommand 'z', -> console.log("Zzzzzz...")
|
||||
Ex = pack.mainModule.provideEx_0_30()
|
||||
Ex.registerCommand
|
||||
name: 'z'
|
||||
priority: 1
|
||||
callback: -> console.log('zzzzzz')
|
||||
```
|
||||
|
||||
See `lib/ex.coffee` for some examples commands. Contributions are very welcome!
|
||||
|
|
|
|||
|
|
@ -1,14 +1,44 @@
|
|||
ExViewModel = require './ex-view-model'
|
||||
Ex = require './ex'
|
||||
ExCommands = require './ex-commands'
|
||||
Find = require './find'
|
||||
CommandError = require './command-error'
|
||||
{getSearchTerm} = require './utils'
|
||||
|
||||
cmp = (x, y) -> if x > y then 1 else if x < y then -1 else 0
|
||||
|
||||
class Command
|
||||
constructor: (@editor, @exState) ->
|
||||
@viewModel = new ExViewModel(@)
|
||||
@vimState = @exState.globalExState.vim?.getEditorState(@editor)
|
||||
|
||||
scanEditor: (term, position, reverse = false) ->
|
||||
return if term is ""
|
||||
|
||||
[rangesBefore, rangesAfter] = [[], []]
|
||||
@editor.scan getSearchTerm(term), ({range}) ->
|
||||
isBefore = if reverse
|
||||
range.start.compare(position) < 0
|
||||
else
|
||||
range.start.compare(position) <= 0
|
||||
|
||||
if isBefore
|
||||
rangesBefore.push(range)
|
||||
else
|
||||
rangesAfter.push(range)
|
||||
|
||||
if reverse
|
||||
rangesAfter.concat(rangesBefore).reverse()[0]
|
||||
else
|
||||
rangesAfter.concat(rangesBefore)[0]
|
||||
|
||||
checkForRepeatSearch: (term, reversed = false) ->
|
||||
if term is '' or reversed and term is '?' or not reversed and term is '/'
|
||||
@vimState.getSearchHistoryItem(0)
|
||||
else
|
||||
term
|
||||
|
||||
parseAddr: (str, curPos) ->
|
||||
if str is '.'
|
||||
if str in ['.', '']
|
||||
addr = curPos.row
|
||||
else if str is '$'
|
||||
# Lines are 0-indexed in Atom, but 1-indexed in vim.
|
||||
|
|
@ -20,18 +50,22 @@ class Command
|
|||
else if str[0] is "'" # Parse Mark...
|
||||
unless @vimState?
|
||||
throw new CommandError("Couldn't get access to vim-mode.")
|
||||
mark = @vimState.marks[str[1]]
|
||||
mark = @vimState.getMark(str[1])
|
||||
unless mark?
|
||||
throw new CommandError("Mark #{str} not set.")
|
||||
addr = mark.bufferMarker.range.end.row
|
||||
else if str[0] is "/"
|
||||
addr = Find.findNextInBuffer(@editor.buffer, curPos, str[1...-1])
|
||||
unless addr?
|
||||
throw new CommandError("Pattern not found: #{str[1...-1]}")
|
||||
else if str[0] is "?"
|
||||
addr = Find.findPreviousInBuffer(@editor.buffer, curPos, str[1...-1])
|
||||
addr = mark.row
|
||||
else if (first = str[0]) in ['/', '?']
|
||||
reversed = first is '?'
|
||||
str = @checkForRepeatSearch(str[1..], reversed)
|
||||
throw new CommandError('No previous regular expression') if not str?
|
||||
str = str[...-1] if str[str.length - 1] is first
|
||||
@regex = str
|
||||
lineRange = @editor.getLastCursor().getCurrentLineBufferRange()
|
||||
pos = if reversed then lineRange.start else lineRange.end
|
||||
addr = @scanEditor(str, pos, reversed)
|
||||
unless addr?
|
||||
throw new CommandError("Pattern not found: #{str[1...-1]}")
|
||||
addr = addr.start.row
|
||||
|
||||
return addr
|
||||
|
||||
|
|
@ -47,26 +81,25 @@ class Command
|
|||
else
|
||||
return -o
|
||||
|
||||
execute: (input) ->
|
||||
@vimState = @exState.globalExState.vim?.getEditorState(@editor)
|
||||
parseLine: (commandLine) ->
|
||||
# Command line parsing (mostly) following the rules at
|
||||
# http://pubs.opengroup.org/onlinepubs/9699919799/utilities
|
||||
# /ex.html#tag_20_40_13_03
|
||||
_commandLine = commandLine
|
||||
|
||||
# Steps 1/2: Leading blanks and colons are ignored.
|
||||
cl = input.characters
|
||||
cl = cl.replace(/^(:|\s)*/, '')
|
||||
return unless cl.length > 0
|
||||
commandLine = commandLine.replace(/^(:|\s)*/, '')
|
||||
return unless commandLine.length > 0
|
||||
|
||||
# Step 3: If the first character is a ", ignore the rest of the line
|
||||
if cl[0] is '"'
|
||||
if commandLine[0] is '"'
|
||||
return
|
||||
|
||||
# Step 4: Address parsing
|
||||
lastLine = @editor.getBuffer().lines.length - 1
|
||||
if cl[0] is '%'
|
||||
if commandLine[0] is '%'
|
||||
range = [0, lastLine]
|
||||
cl = cl[1..]
|
||||
commandLine = commandLine[1..]
|
||||
else
|
||||
addrPattern = ///^
|
||||
(?: # First address
|
||||
|
|
@ -75,8 +108,8 @@ class Command
|
|||
\$| # Last line
|
||||
\d+| # n-th line
|
||||
'[\[\]<>'`"^.(){}a-zA-Z]| # Marks
|
||||
/.*?[^\\]/| # Regex
|
||||
\?.*?[^\\]\?| # Backwards search
|
||||
/(?:.*?[^\\]|)(?:/|$)| # Regex
|
||||
\?(?:.*?[^\\]|)(?:\?|$)| # Backwards search
|
||||
[+-]\d* # Current line +/- a number of lines
|
||||
)((?:\s*[+-]\d*)*) # Line offset
|
||||
)?
|
||||
|
|
@ -86,14 +119,15 @@ class Command
|
|||
\$|
|
||||
\d+|
|
||||
'[\[\]<>'`"^.(){}a-zA-Z]|
|
||||
/.*?[^\\]/|
|
||||
\?.*?[^\\]\?|
|
||||
[+-]\d*
|
||||
)((?:\s*[+-]\d*)*)
|
||||
/(?:.*?[^\\]|)(?:/|$)|
|
||||
\?(?:.*?[^\\]|)(?:\?|$)|
|
||||
[+-]\d*|
|
||||
# Empty second address
|
||||
)((?:\s*[+-]\d*)*)|
|
||||
)?
|
||||
///
|
||||
|
||||
[match, addr1, off1, addr2, off2] = cl.match(addrPattern)
|
||||
[match, addr1, off1, addr2, off2] = commandLine.match(addrPattern)
|
||||
|
||||
curPos = @editor.getCursorBufferPosition()
|
||||
|
||||
|
|
@ -115,6 +149,9 @@ class Command
|
|||
if off2?
|
||||
address2 += @parseOffset(off2)
|
||||
|
||||
if @regex?
|
||||
@vimState.pushSearchHistory(@regex)
|
||||
|
||||
if address2 < 0 or address2 > lastLine
|
||||
throw new CommandError('Invalid range')
|
||||
|
||||
|
|
@ -122,15 +159,15 @@ class Command
|
|||
throw new CommandError('Backwards range given')
|
||||
|
||||
range = [address1, if address2? then address2 else address1]
|
||||
cl = cl[match?.length..]
|
||||
commandLine = commandLine[match?.length..]
|
||||
|
||||
# Step 5: Leading blanks are ignored
|
||||
cl = cl.trimLeft()
|
||||
commandLine = commandLine.trimLeft()
|
||||
|
||||
# Step 6a: If no command is specified, go to the last specified address
|
||||
if cl.length is 0
|
||||
if commandLine.length is 0
|
||||
@editor.setCursorBufferPosition([range[1], 0])
|
||||
return
|
||||
return {range, command: undefined, args: undefined}
|
||||
|
||||
# Ignore steps 6b and 6c since they only make sense for print commands and
|
||||
# print doesn't make sense
|
||||
|
|
@ -139,31 +176,39 @@ class Command
|
|||
|
||||
# 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])
|
||||
if commandLine.length is 2 and commandLine[0] is 'k' \
|
||||
and /[a-z]/i.test(commandLine[1])
|
||||
command = 'mark'
|
||||
args = cl[1]
|
||||
else if not /[a-z]/i.test(cl[0])
|
||||
command = cl[0]
|
||||
args = cl[1..]
|
||||
args = commandLine[1]
|
||||
else if not /[a-z]/i.test(commandLine[0])
|
||||
command = commandLine[0]
|
||||
args = commandLine[1..]
|
||||
else
|
||||
[m, command, args] = cl.match(/^(\w+)(.*)/)
|
||||
[m, command, args] = commandLine.match(/^(\w+)(.*)/)
|
||||
|
||||
# If the command matches an existing one exactly, execute that one
|
||||
if (func = Ex.singleton()[command])?
|
||||
func(range, args)
|
||||
commandLineRE = new RegExp("^" + command)
|
||||
matching = []
|
||||
|
||||
for name in Object.keys(ExCommands.commands)
|
||||
if commandLineRE.test(name)
|
||||
command = ExCommands.commands[name]
|
||||
if matching.length is 0
|
||||
matching = [command]
|
||||
else
|
||||
# Step 8: Match command against existing commands
|
||||
matching = (name for name, val of Ex.singleton() when \
|
||||
name.indexOf(command) is 0)
|
||||
switch cmp(command.priority, matching[0].priority)
|
||||
when 1 then matching = [command]
|
||||
when 0 then matching.push(command)
|
||||
|
||||
matching.sort()
|
||||
command = matching.sort()[0]
|
||||
unless command?
|
||||
throw new CommandError("Not an editor command: #{_commandLine}")
|
||||
|
||||
command = matching[0]
|
||||
return {command: command.callback, range, args: args.trimLeft()}
|
||||
|
||||
func = Ex.singleton()[command]
|
||||
if func?
|
||||
func(range, args)
|
||||
else
|
||||
throw new CommandError("Not an editor command: #{input.characters}")
|
||||
|
||||
execute: (input) ->
|
||||
{command, range, args} = @parseLine(input.characters)
|
||||
|
||||
command?({args, range, @editor, @exState, @vimState})
|
||||
|
||||
module.exports = Command
|
||||
|
|
|
|||
321
lib/ex-commands.coffee
Normal file
321
lib/ex-commands.coffee
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
path = require 'path'
|
||||
CommandError = require './command-error'
|
||||
fs = require 'fs-plus'
|
||||
VimOption = require './vim-option'
|
||||
{getSearchTerm} = require './utils'
|
||||
|
||||
trySave = (func) ->
|
||||
deferred = Promise.defer()
|
||||
|
||||
try
|
||||
func()
|
||||
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 = atom.workspace.getActiveTextEditor()
|
||||
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
|
||||
|
||||
module.exports =
|
||||
class ExCommands
|
||||
@commands =
|
||||
'quit':
|
||||
priority: 1000
|
||||
callback: ->
|
||||
atom.workspace.getActivePane().destroyActiveItem()
|
||||
'tabclose':
|
||||
priority: 1000
|
||||
callback: (ev) =>
|
||||
@callCommand('quit', ev)
|
||||
'qall':
|
||||
priority: 1000
|
||||
callback: ->
|
||||
atom.close()
|
||||
'tabnext':
|
||||
priority: 1000
|
||||
callback: ->
|
||||
atom.workspace.getActivePane().activateNextItem()
|
||||
'tabprevious':
|
||||
priority: 1000
|
||||
callback: ->
|
||||
atom.workspace.getActivePane().activatePreviousItem()
|
||||
'write':
|
||||
priority: 1001
|
||||
callback: ({editor, args}) ->
|
||||
if args[0] is '!'
|
||||
force = true
|
||||
args = args[1..]
|
||||
|
||||
filePath = args.trimLeft()
|
||||
if /[^\\] /.test(filePath)
|
||||
throw new CommandError('Only one file name allowed')
|
||||
filePath = filePath.replace(/\\ /g, ' ')
|
||||
|
||||
deferred = Promise.defer()
|
||||
|
||||
if filePath.length isnt 0
|
||||
fullPath = getFullPath(filePath)
|
||||
else if editor.getPath()?
|
||||
trySave(-> editor.save())
|
||||
.then(deferred.resolve)
|
||||
else
|
||||
fullPath = atom.showSaveDialogSync()
|
||||
|
||||
if fullPath?
|
||||
if not force and fs.existsSync(fullPath)
|
||||
throw new CommandError("File exists (add ! to override)")
|
||||
trySave(-> saveAs(fullPath, editor))
|
||||
.then(deferred.resolve)
|
||||
|
||||
deferred.promise
|
||||
'update':
|
||||
priority: 1000
|
||||
callback: (ev) =>
|
||||
@callCommand('write', ev)
|
||||
'wall':
|
||||
priority: 1000
|
||||
callback: ->
|
||||
# FIXME: This is undocumented for quite a while now - not even
|
||||
# deprecated. Should probably use PaneContainer::saveAll
|
||||
atom.workspace.saveAll()
|
||||
'wq':
|
||||
priority: 1000
|
||||
callback: (ev) =>
|
||||
@callCommand('write', ev).then => @callCommand('quit')
|
||||
'xit':
|
||||
priority: 1000
|
||||
callback: (ev) =>
|
||||
@callCommand('wq', ev)
|
||||
'exit':
|
||||
priority: 1000
|
||||
callback: (ev) => @callCommand('xit', ev)
|
||||
'xall':
|
||||
priority: 1000
|
||||
callback: (ev) =>
|
||||
atom.workspace.saveAll()
|
||||
@callCommand('qall', ev)
|
||||
'edit':
|
||||
priority: 1001
|
||||
callback: ({args, editor}) ->
|
||||
args = args.trim()
|
||||
if args[0] is '!'
|
||||
force = true
|
||||
args = args[1..]
|
||||
|
||||
if editor.isModified() and not force
|
||||
throw new CommandError(
|
||||
'No write since last change (add ! to override)')
|
||||
|
||||
filePath = args.trimLeft()
|
||||
if /[^\\] /.test(filePath)
|
||||
throw new CommandError('Only one file name allowed')
|
||||
filePath = filePath.replace(/\\ /g, ' ')
|
||||
|
||||
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')
|
||||
'tabedit':
|
||||
priority: 1000
|
||||
callback: (ev) =>
|
||||
if ev.args.trim() is ''
|
||||
@callCommand('tabnew', ev)
|
||||
else
|
||||
@callCommand('edit', ev)
|
||||
'tabnew':
|
||||
priority: 1000
|
||||
callback: ->
|
||||
atom.workspace.open()
|
||||
'split':
|
||||
priority: 1000
|
||||
callback: ({args}) ->
|
||||
filePath = args.trim()
|
||||
if /[^\\] /.test(filePath)
|
||||
throw new CommandError('Only one file name allowed')
|
||||
filePath = filePath.replace(/\\ /g, ' ')
|
||||
|
||||
pane = atom.workspace.getActivePane()
|
||||
if filePath.length isnt 0
|
||||
# FIXME: This is horribly slow
|
||||
atom.workspace.openURIInPane(getFullPath(filePath), pane.splitUp())
|
||||
else
|
||||
pane.splitUp(copyActiveItem: true)
|
||||
'new':
|
||||
priority: 1000
|
||||
callback: ({args}) ->
|
||||
filePath = args.trim()
|
||||
if /[^\\] /.test(filePath)
|
||||
throw new CommandError('Only one file name allowed')
|
||||
filePath = filePath.replace(/\\ /g, ' ')
|
||||
filePath = undefined if filePath.length is 0
|
||||
# FIXME: This is horribly slow
|
||||
atom.workspace.openURIInPane(filePath,
|
||||
atom.workspace.getActivePane().splitUp())
|
||||
'vsplit':
|
||||
priority: 1000
|
||||
callback: ({args}) ->
|
||||
filePath = args.trim()
|
||||
if /[^\\] /.test(filePath)
|
||||
throw new CommandError('Only one file name allowed')
|
||||
filePath = filePath.replace(/\\ /g, ' ')
|
||||
|
||||
pane = atom.workspace.getActivePane()
|
||||
if filePath.length isnt 0
|
||||
# FIXME: This is horribly slow
|
||||
atom.workspace.openURIInPane(getFullPath(filePath),
|
||||
pane.splitLeft())
|
||||
else
|
||||
pane.splitLeft(copyActiveItem: true)
|
||||
'vnew':
|
||||
priority: 1000
|
||||
callback: ({args}) ->
|
||||
filePath = args.trim()
|
||||
if /[^\\] /.test(filePath)
|
||||
throw new CommandError('Only one file name allowed')
|
||||
filePath = filePath.replace(/\\ /g, ' ')
|
||||
filePath = undefined if filePath.length is 0
|
||||
# FIXME: This is horribly slow
|
||||
atom.workspace.openURIInPane(filePath,
|
||||
atom.workspace.getActivePane().splitLeft())
|
||||
'delete':
|
||||
priority: 1000
|
||||
callback: ({range, editor}) ->
|
||||
range = [[range[0], 0], [range[1] + 1, 0]]
|
||||
editor.setTextInBufferRange(range, '')
|
||||
'substitute':
|
||||
priority: 1001
|
||||
callback: ({range, args, editor, vimState}) ->
|
||||
args_ = args.trimLeft()
|
||||
delim = args_[0]
|
||||
if /[a-z]/i.test(delim)
|
||||
throw new CommandError(
|
||||
"Regular expressions can't be delimited by letters")
|
||||
if delim is '\\'
|
||||
throw new CommandError(
|
||||
"Regular expressions can't be delimited by \\")
|
||||
args_ = args_[1..]
|
||||
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
|
||||
escaped = false
|
||||
parsed[parsing] += char
|
||||
|
||||
[pattern, substition, flags] = parsed
|
||||
if pattern is ''
|
||||
pattern = vimState.getSearchHistoryItem(0)
|
||||
if not pattern?
|
||||
atom.beep()
|
||||
throw new CommandError('No previous regular expression')
|
||||
else
|
||||
vimState.pushSearchHistory(pattern)
|
||||
|
||||
try
|
||||
flagsObj = {}
|
||||
flags.split('').forEach((flag) -> flagsObj[flag] = true)
|
||||
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))
|
||||
)
|
||||
'set':
|
||||
priority: 1000
|
||||
callback: ({args}) ->
|
||||
if args is ''
|
||||
throw new CommandError('No option specified')
|
||||
options = args.split(' ')
|
||||
for option in options
|
||||
if option[...2] is 'no'
|
||||
VimOption.set(option[2..], false)
|
||||
else if option[...3] is 'inv'
|
||||
VimOption.inv(option[3..])
|
||||
else if option[-1..] is '!'
|
||||
VimOption.inv(option[...-1])
|
||||
else
|
||||
VimOption.set(option, true)
|
||||
|
||||
@registerCommand: ({name, priority, callback}) =>
|
||||
@commands[name] = {priority, callback}
|
||||
|
||||
@callCommand: (name, ev) =>
|
||||
@commands[name].callback(ev)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
GlobalExState = require './global-ex-state'
|
||||
ExState = require './ex-state'
|
||||
Ex = require './ex'
|
||||
ExCommands = require './ex-commands'
|
||||
{Disposable, CompositeDisposable} = require 'event-kit'
|
||||
|
||||
module.exports = ExMode =
|
||||
|
|
@ -22,14 +22,18 @@ module.exports = ExMode =
|
|||
|
||||
@exStates.set(editor, exState)
|
||||
|
||||
@disposables.add new Disposable =>
|
||||
@disposables.add new Disposable ->
|
||||
exState.destroy()
|
||||
|
||||
deactivate: ->
|
||||
@disposables.dispose()
|
||||
|
||||
provideEx: ->
|
||||
registerCommand: Ex.registerCommand.bind(Ex)
|
||||
provideEx_0_20: ->
|
||||
registerCommand: (name, callback) ->
|
||||
ExCommands.registerCommand({name, callback, priority: 1})
|
||||
|
||||
provideEx_0_30: ->
|
||||
registerCommand: ExCommands.registerCommand
|
||||
|
||||
consumeVim: (vim) ->
|
||||
@vim = vim
|
||||
|
|
|
|||
287
lib/ex.coffee
287
lib/ex.coffee
|
|
@ -1,287 +0,0 @@
|
|||
path = require 'path'
|
||||
CommandError = require './command-error'
|
||||
fs = require 'fs-plus'
|
||||
VimOption = require './vim-option'
|
||||
|
||||
trySave = (func) ->
|
||||
deferred = Promise.defer()
|
||||
|
||||
try
|
||||
func()
|
||||
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 = atom.workspace.getActiveTextEditor()
|
||||
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
|
||||
|
||||
class Ex
|
||||
@singleton: =>
|
||||
@ex ||= new Ex
|
||||
|
||||
@registerCommand: (name, func) =>
|
||||
@singleton()[name] = func
|
||||
|
||||
quit: ->
|
||||
atom.workspace.getActivePane().destroyActiveItem()
|
||||
|
||||
q: => @quit()
|
||||
|
||||
tabedit: (range, args) =>
|
||||
if args.trim() isnt ''
|
||||
@edit(range, args)
|
||||
else
|
||||
@tabnew(range, args)
|
||||
|
||||
tabe: (args...) => @tabedit(args...)
|
||||
|
||||
tabnew: (range, args) =>
|
||||
if args.trim() is ''
|
||||
atom.workspace.open()
|
||||
else
|
||||
@tabedit(range, 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()
|
||||
|
||||
edit: (range, filePath) ->
|
||||
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
|
||||
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: ->
|
||||
buffer = atom.workspace.getActiveTextEditor().buffer
|
||||
buffer.setPath(undefined)
|
||||
buffer.load()
|
||||
|
||||
write: (range, filePath) ->
|
||||
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 = Promise.defer()
|
||||
|
||||
editor = atom.workspace.getActiveTextEditor()
|
||||
saved = false
|
||||
if filePath.length isnt 0
|
||||
fullPath = getFullPath(filePath)
|
||||
if editor.getPath()? and (not fullPath? or editor.getPath() == fullPath)
|
||||
# Use editor.save when no path is given or the path to the file is given
|
||||
trySave(-> editor.save()).then(deferred.resolve)
|
||||
saved = true
|
||||
else if not fullPath?
|
||||
fullPath = atom.showSaveDialogSync()
|
||||
|
||||
if not saved and fullPath?
|
||||
if not force and fs.existsSync(fullPath)
|
||||
throw new CommandError("File exists (add ! to override)")
|
||||
trySave(-> saveAs(fullPath)).then(deferred.resolve)
|
||||
|
||||
deferred.promise
|
||||
|
||||
w: (args...) =>
|
||||
@write(args...)
|
||||
|
||||
wq: (args...) =>
|
||||
@write(args...).then => @quit()
|
||||
|
||||
xit: (args...) => @wq(args...)
|
||||
|
||||
wa: ->
|
||||
atom.workspace.saveAll()
|
||||
|
||||
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 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) ->
|
||||
args = args.trimLeft()
|
||||
delim = args[0]
|
||||
if /[a-z]/i.test(delim)
|
||||
throw new CommandError(
|
||||
"Regular expressions can't be delimited by letters")
|
||||
delimRE = new RegExp("[^\\\\]#{delim}")
|
||||
spl = []
|
||||
args_ = args[1..]
|
||||
while (i = args_.search(delimRE)) isnt -1
|
||||
spl.push args_[..i]
|
||||
args_ = args_[i + 2..]
|
||||
if args_.length is 0 and spl.length is 3
|
||||
throw new CommandError('Trailing characters')
|
||||
else if args_.length isnt 0
|
||||
spl.push args_
|
||||
if spl.length > 3
|
||||
throw new CommandError('Trailing characters')
|
||||
spl[1] ?= ''
|
||||
spl[2] ?= ''
|
||||
notDelimRE = new RegExp("\\\\#{delim}", 'g')
|
||||
spl[0] = spl[0].replace(notDelimRE, delim)
|
||||
spl[1] = spl[1].replace(notDelimRE, delim)
|
||||
|
||||
try
|
||||
pattern = new RegExp(spl[0], spl[2])
|
||||
catch e
|
||||
if e.message.indexOf('Invalid flags supplied to RegExp constructor') is 0
|
||||
# vim only says 'Trailing characters', but let's be more descriptive
|
||||
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
|
||||
|
||||
buffer = atom.workspace.getActiveTextEditor().buffer
|
||||
atom.workspace.getActiveTextEditor().transact ->
|
||||
for line in [range[0]..range[1]]
|
||||
buffer.scanInRange(pattern,
|
||||
[[line, 0], [line, buffer.lines[line].length]],
|
||||
({match, matchText, range, stop, replace}) ->
|
||||
replace(replaceGroups(match[..], spl[1]))
|
||||
)
|
||||
|
||||
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 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]]
|
||||
atom.workspace.getActiveTextEditor().buffer.setTextInRange(range, '')
|
||||
|
||||
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()
|
||||
|
||||
module.exports = Ex
|
||||
36
lib/utils.coffee
Normal file
36
lib/utils.coffee
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
_ = require 'underscore-plus'
|
||||
|
||||
module.exports =
|
||||
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)
|
||||
|
|
@ -1,23 +1,19 @@
|
|||
class VimOption
|
||||
@singleton: =>
|
||||
@option ||= new VimOption
|
||||
@options =
|
||||
'list': 'editor.showInvisibles'
|
||||
'nu': 'editor.showLineNumbers'
|
||||
'number': 'editor.showLineNumbers'
|
||||
|
||||
list: =>
|
||||
atom.config.set("editor.showInvisibles", true)
|
||||
@registerOption: (vimName, atomName) ->
|
||||
@options[vimName] = atomName
|
||||
|
||||
nolist: =>
|
||||
atom.config.set("editor.showInvisibles", false)
|
||||
@set: (name, value) ->
|
||||
atom.config.set(@options[name], value)
|
||||
|
||||
number: =>
|
||||
atom.config.set("editor.showLineNumbers", true)
|
||||
@get: (name) ->
|
||||
atom.config.get(@options[name])
|
||||
|
||||
nu: =>
|
||||
@number()
|
||||
|
||||
nonumber: =>
|
||||
atom.config.set("editor.showLineNumbers", false)
|
||||
|
||||
nonu: =>
|
||||
@nonumber()
|
||||
@inv: (name) ->
|
||||
atom.config.set(@options[name], not atom.config.get(@options[name]))
|
||||
|
||||
module.exports = VimOption
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@
|
|||
"ex-mode": {
|
||||
"description": "Ex commands",
|
||||
"versions": {
|
||||
"0.20.0": "provideEx"
|
||||
"0.20.0": "provideEx_0_20",
|
||||
"0.30.0": "provideEx_0_30"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
185
spec/command-parsing-spec.coffee
Normal file
185
spec/command-parsing-spec.coffee
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
helpers = require './spec-helper'
|
||||
Command = require '../lib/command'
|
||||
ExCommands = require('../lib/ex-commands')
|
||||
|
||||
describe "command parsing", ->
|
||||
[editor, editorElement, vimState, exState, command, lines] = []
|
||||
beforeEach ->
|
||||
vimMode = atom.packages.loadPackage('vim-mode')
|
||||
exMode = atom.packages.loadPackage('ex-mode')
|
||||
exMode.activate()
|
||||
|
||||
waitsForPromise ->
|
||||
vimMode.activate().then ->
|
||||
helpers.activateExMode().then ->
|
||||
helpers.getEditorElement (element) ->
|
||||
editorElement = element
|
||||
editor = editorElement.getModel()
|
||||
atom.commands.dispatch(element, 'ex-mode:open')
|
||||
atom.commands.dispatch(editor.normalModeInputView.editorElement,
|
||||
'core:cancel')
|
||||
vimState = vimMode.mainModule.getEditorState(editor)
|
||||
exState = exMode.mainModule.exStates.get(editor)
|
||||
command = new Command(editor, exState)
|
||||
vimState.activateNormalMode()
|
||||
vimState.resetNormalMode()
|
||||
editor.setText(
|
||||
'abc\nabc\nabc\nabc\nabc\nabc\nabc\nabc\nabc\nabc\nabc\nabc'
|
||||
'\nabc\nabc\n')
|
||||
lines = editor.getBuffer().getLines()
|
||||
editor.setCursorBufferPosition([0, 0])
|
||||
|
||||
it "parses a simple command (e.g. `:quit`)", ->
|
||||
expect(command.parseLine('quit')).toEqual
|
||||
command: ExCommands.commands.quit.callback
|
||||
args: ''
|
||||
range: [0, 0]
|
||||
|
||||
it "matches sub-commands (e.g. `:q`)", ->
|
||||
expect(command.parseLine('q')).toEqual
|
||||
command: ExCommands.commands.quit.callback
|
||||
args: ''
|
||||
range: [0, 0]
|
||||
|
||||
it "uses the command with the highest priority if multiple match an input", ->
|
||||
expect(command.parseLine('s').command)
|
||||
.toEqual(ExCommands.commands.substitute.callback)
|
||||
|
||||
it "ignores leading blanks and spaces", ->
|
||||
expect(command.parseLine(':::: :::: : : : ')).toBeUndefined
|
||||
expect(command.parseLine(':: :::::: :quit')).toEqual
|
||||
command: ExCommands.commands.quit.callback
|
||||
args: ''
|
||||
range: [0, 0]
|
||||
expect(atom.notifications.notifications.length).toBe(0)
|
||||
|
||||
it 'ignores the line if it starts with a "', ->
|
||||
expect(command.parseLine('"quit')).toBe(undefined)
|
||||
expect(atom.notifications.notifications.length).toBe(0)
|
||||
|
||||
describe "address parsing", ->
|
||||
describe "with only one address", ->
|
||||
it "parses . as an address", ->
|
||||
expect(command.parseLine('.').range).toEqual([0, 0])
|
||||
editor.setCursorBufferPosition([2, 0])
|
||||
expect(command.parseLine('.').range).toEqual([2, 2])
|
||||
|
||||
it "parses $ as an address", ->
|
||||
expect(command.parseLine('$').range)
|
||||
.toEqual([lines.length - 1, lines.length - 1])
|
||||
|
||||
it "parses a number as an address", ->
|
||||
expect(command.parseLine('3').range).toEqual([2, 2])
|
||||
expect(command.parseLine('7').range).toEqual([6, 6])
|
||||
|
||||
it "parses 'a as an address", ->
|
||||
vimState.setMark('a', [3, 1])
|
||||
expect(command.parseLine("'a").range).toEqual([3, 3])
|
||||
|
||||
it "throws an error if the mark is not set", ->
|
||||
vimState.marks.a = undefined
|
||||
expect(-> command.parseLine("'a")).toThrow()
|
||||
|
||||
it "parses /a and ?a as addresses", ->
|
||||
expect(command.parseLine('/abc').range).toEqual([1, 1])
|
||||
editor.setCursorBufferPosition([1, 0])
|
||||
expect(command.parseLine('?abc').range).toEqual([0, 0])
|
||||
editor.setCursorBufferPosition([0, 0])
|
||||
expect(command.parseLine('/bc').range).toEqual([1, 1])
|
||||
|
||||
it "integrates the search history for :/", ->
|
||||
vimState.pushSearchHistory('abc')
|
||||
expect(command.parseLine('//').range).toEqual([1, 1])
|
||||
command.parseLine('/ab/,/bc/+2')
|
||||
expect(vimState.getSearchHistoryItem(0)).toEqual('bc')
|
||||
|
||||
describe "case sensitivity for search patterns", ->
|
||||
beforeEach ->
|
||||
editor.setText('abca\nABC\ndefdDEF\nabcaABC')
|
||||
|
||||
describe "respects the smartcase setting", ->
|
||||
describe "with smartcasse off", ->
|
||||
beforeEach ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', false)
|
||||
editor.setCursorBufferPosition([0, 0])
|
||||
|
||||
it "uses case sensitive search if pattern is lowercase", ->
|
||||
expect(command.parseLine('/abc').range).toEqual([3, 3])
|
||||
|
||||
it "uses case sensitive search if the pattern is uppercase", ->
|
||||
expect(command.parseLine('/ABC').range).toEqual([1, 1])
|
||||
|
||||
describe "with smartcase on", ->
|
||||
beforeEach ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', true)
|
||||
|
||||
it "uses case insensitive search if the pattern is lowercase", ->
|
||||
editor.setCursorBufferPosition([0, 0])
|
||||
expect(command.parseLine('/abc').range).toEqual([1, 1])
|
||||
|
||||
it "uses case sensitive search if the pattern is uppercase", ->
|
||||
editor.setCursorBufferPosition([3, 3])
|
||||
expect(command.parseLine('/ABC').range).toEqual([1, 1])
|
||||
|
||||
describe "\\c and \\C", ->
|
||||
describe "only \\c in the pattern", ->
|
||||
beforeEach ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', false)
|
||||
editor.setCursorBufferPosition([0, 0])
|
||||
|
||||
it "uses case insensitive search if smartcase is off", ->
|
||||
expect(command.parseLine('/abc\\c').range).toEqual([1, 1])
|
||||
|
||||
it "doesn't matter where it is", ->
|
||||
expect(command.parseLine('/ab\\cc').range).toEqual([1, 1])
|
||||
|
||||
describe "only \\C in the pattern with smartcase on", ->
|
||||
beforeEach ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', true)
|
||||
editor.setCursorBufferPosition([0, 0])
|
||||
|
||||
it "uses case sensitive search if the pattern is lowercase", ->
|
||||
expect(command.parseLine('/abc\\C').range).toEqual([3, 3])
|
||||
|
||||
it "doesn't matter where it is", ->
|
||||
expect(command.parseLine('/ab\\Cc').range).toEqual([3, 3])
|
||||
|
||||
describe "with \\c and \\C in the pattern", ->
|
||||
beforeEach ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', false)
|
||||
editor.setCursorBufferPosition([0, 0])
|
||||
|
||||
it "uses case insensitive search if \\C comes first", ->
|
||||
expect(command.parseLine('/a\\Cb\\cc').range).toEqual([1, 1])
|
||||
|
||||
it "uses case insensitive search if \\c comes first", ->
|
||||
expect(command.parseLine('/a\\cb\\Cc').range).toEqual([1, 1])
|
||||
|
||||
describe "with two addresses", ->
|
||||
it "parses both", ->
|
||||
expect(command.parseLine('5,10').range).toEqual([4, 9])
|
||||
|
||||
it "throws an error if it is in reverse order", ->
|
||||
expect(-> command.parseLine('10,5').range).toThrow()
|
||||
|
||||
it "uses the current line as second address if empty", ->
|
||||
editor.setCursorBufferPosition([3, 0])
|
||||
expect(command.parseLine('-2,').range).toEqual([1, 3])
|
||||
|
||||
it "parses a command with a range and no arguments", ->
|
||||
expect(command.parseLine('2,/abc/+4delete')).toEqual
|
||||
command: ExCommands.commands.delete.callback
|
||||
args: ''
|
||||
range: [1, 5]
|
||||
|
||||
it "parses a command with no range and arguments", ->
|
||||
expect(command.parseLine('edit edit-test test-2')).toEqual
|
||||
command: ExCommands.commands.edit.callback
|
||||
args: 'edit-test test-2'
|
||||
range: [0, 0]
|
||||
|
||||
it "parses a command with range and arguments", ->
|
||||
expect(command.parseLine('3,5+2s/abc/def/gi')).toEqual
|
||||
command: ExCommands.commands.substitute.callback
|
||||
args: '/abc/def/gi'
|
||||
range: [2, 6]
|
||||
|
|
@ -3,8 +3,7 @@ path = require 'path'
|
|||
os = require 'os'
|
||||
uuid = require 'node-uuid'
|
||||
helpers = require './spec-helper'
|
||||
|
||||
Ex = require('../lib/ex').singleton()
|
||||
ExCommands = require('../lib/ex-commands')
|
||||
|
||||
describe "the commands", ->
|
||||
[editor, editorElement, vimState, exState, dir, dir2] = []
|
||||
|
|
@ -177,11 +176,25 @@ describe "the commands", ->
|
|||
|
||||
describe ":tabclose", ->
|
||||
it "acts as an alias to :quit", ->
|
||||
spyOn(Ex, 'tabclose').andCallThrough()
|
||||
spyOn(Ex, 'quit').andCallThrough()
|
||||
spyOn(ExCommands.commands.tabclose, 'callback').andCallThrough()
|
||||
spyOn(ExCommands.commands.quit, 'callback').andCallThrough()
|
||||
keydown(':')
|
||||
submitNormalModeInputText('tabclose')
|
||||
expect(Ex.quit).toHaveBeenCalledWith(Ex.tabclose.calls[0].args...)
|
||||
expect(ExCommands.commands.quit.callback)
|
||||
.toHaveBeenCalledWith(ExCommands.commands.tabclose.callback
|
||||
.calls[0].args[0])
|
||||
|
||||
describe ":qall", ->
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open().then -> atom.workspace.open()
|
||||
.then -> atom.workspace.open()
|
||||
|
||||
it "closes the window", ->
|
||||
spyOn(atom, 'close')
|
||||
keydown(':')
|
||||
submitNormalModeInputText('qall')
|
||||
expect(atom.close).toHaveBeenCalled()
|
||||
|
||||
describe ":tabnext", ->
|
||||
pane = null
|
||||
|
|
@ -223,45 +236,81 @@ describe "the commands", ->
|
|||
submitNormalModeInputText('tabprevious')
|
||||
expect(pane.getActiveItemIndex()).toBe(pane.getItems().length - 1)
|
||||
|
||||
describe ":update", ->
|
||||
it "acts as an alias to :write", ->
|
||||
spyOn(ExCommands.commands.update, 'callback')
|
||||
.andCallThrough()
|
||||
spyOn(ExCommands.commands.write, 'callback')
|
||||
keydown(':')
|
||||
submitNormalModeInputText('update')
|
||||
expect(ExCommands.commands.write.callback).toHaveBeenCalledWith(
|
||||
ExCommands.commands.update.callback.calls[0].args[0])
|
||||
|
||||
describe ":wall", ->
|
||||
it "saves all open files", ->
|
||||
spyOn(atom.workspace, 'saveAll')
|
||||
keydown(':')
|
||||
submitNormalModeInputText('wall')
|
||||
expect(atom.workspace.saveAll).toHaveBeenCalled()
|
||||
|
||||
describe ":wq", ->
|
||||
beforeEach ->
|
||||
spyOn(Ex, 'write').andCallThrough()
|
||||
spyOn(Ex, 'quit')
|
||||
spyOn(ExCommands.commands.write, 'callback').andCallThrough()
|
||||
spyOn(ExCommands.commands.quit, 'callback')
|
||||
|
||||
it "writes the file, then quits", ->
|
||||
spyOn(atom, 'showSaveDialogSync').andReturn(projectPath('wq-1'))
|
||||
keydown(':')
|
||||
submitNormalModeInputText('wq')
|
||||
expect(Ex.write).toHaveBeenCalled()
|
||||
expect(ExCommands.commands.write.callback).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)
|
||||
waitsFor((-> ExCommands.commands.quit.callback.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()
|
||||
expect(ExCommands.commands.write.callback).toHaveBeenCalled()
|
||||
wasNotCalled = false
|
||||
# FIXME: This seems dangerous, but setTimeout somehow doesn't work.
|
||||
setImmediate((->
|
||||
wasNotCalled = not Ex.quit.wasCalled))
|
||||
wasNotCalled = not ExCommands.commands.quit.callback.wasCalled))
|
||||
waitsFor((-> wasNotCalled), 100)
|
||||
|
||||
it "passes the file name", ->
|
||||
keydown(':')
|
||||
submitNormalModeInputText('wq wq-2')
|
||||
expect(Ex.write)
|
||||
expect(ExCommands.commands.write.callback)
|
||||
.toHaveBeenCalled()
|
||||
expect(Ex.write.calls[0].args[1].trim()).toEqual('wq-2')
|
||||
waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100)
|
||||
expect(ExCommands.commands.write.callback.calls[0].args[0].args)
|
||||
.toEqual('wq-2')
|
||||
waitsFor((-> ExCommands.commands.quit.callback.wasCalled),
|
||||
"the :quit command to be called", 100)
|
||||
|
||||
describe ":xit", ->
|
||||
it "acts as an alias to :wq", ->
|
||||
spyOn(Ex, 'wq')
|
||||
spyOn(ExCommands.commands.wq, 'callback')
|
||||
keydown(':')
|
||||
submitNormalModeInputText('xit')
|
||||
expect(Ex.wq).toHaveBeenCalled()
|
||||
expect(ExCommands.commands.wq.callback).toHaveBeenCalled()
|
||||
|
||||
describe ":exit", ->
|
||||
it "is an alias to :xit", ->
|
||||
spyOn(ExCommands.commands.xit, 'callback')
|
||||
keydown(':')
|
||||
submitNormalModeInputText('exit')
|
||||
expect(ExCommands.commands.xit.callback).toHaveBeenCalled()
|
||||
|
||||
describe ":xall", ->
|
||||
it "saves all open files and closes the window", ->
|
||||
spyOn(atom.workspace, 'saveAll')
|
||||
spyOn(atom, 'close')
|
||||
keydown(':')
|
||||
submitNormalModeInputText('xall')
|
||||
expect(atom.workspace.saveAll).toHaveBeenCalled()
|
||||
expect(atom.close).toHaveBeenCalled()
|
||||
|
||||
describe ":edit", ->
|
||||
describe "without a file name", ->
|
||||
|
|
@ -339,19 +388,20 @@ describe "the commands", ->
|
|||
|
||||
describe ":tabedit", ->
|
||||
it "acts as an alias to :edit if supplied with a path", ->
|
||||
spyOn(Ex, 'tabedit').andCallThrough()
|
||||
spyOn(Ex, 'edit')
|
||||
spyOn(ExCommands.commands.tabedit, 'callback').andCallThrough()
|
||||
spyOn(ExCommands.commands.edit, 'callback')
|
||||
keydown(':')
|
||||
submitNormalModeInputText('tabedit tabedit-test')
|
||||
expect(Ex.edit).toHaveBeenCalledWith(Ex.tabedit.calls[0].args...)
|
||||
expect(ExCommands.commands.edit.callback).toHaveBeenCalledWith(
|
||||
ExCommands.commands.tabedit.callback.calls[0].args...)
|
||||
|
||||
it "acts as an alias to :tabnew if not supplied with a path", ->
|
||||
spyOn(Ex, 'tabedit').andCallThrough()
|
||||
spyOn(Ex, 'tabnew')
|
||||
spyOn(ExCommands.commands.tabedit, 'callback').andCallThrough()
|
||||
spyOn(ExCommands.commands.tabnew, 'callback')
|
||||
keydown(':')
|
||||
submitNormalModeInputText('tabedit ')
|
||||
expect(Ex.tabnew)
|
||||
.toHaveBeenCalledWith(Ex.tabedit.calls[0].args...)
|
||||
expect(ExCommands.commands.tabnew.callback).toHaveBeenCalledWith(
|
||||
ExCommands.commands.tabedit.callback.calls[0].args...)
|
||||
|
||||
describe ":tabnew", ->
|
||||
it "opens a new tab", ->
|
||||
|
|
@ -372,6 +422,15 @@ describe "the commands", ->
|
|||
# FIXME: Should test whether the new pane contains a TextEditor
|
||||
# pointing to the same path
|
||||
|
||||
describe ":new", ->
|
||||
it "splits a new file upwards", ->
|
||||
pane = atom.workspace.getActivePane()
|
||||
spyOn(pane, 'splitUp').andCallThrough()
|
||||
keydown(':')
|
||||
submitNormalModeInputText('new')
|
||||
expect(pane.splitUp).toHaveBeenCalled()
|
||||
# FIXME: Should test whether the new pane contains an empty file
|
||||
|
||||
describe ":vsplit", ->
|
||||
it "splits the current file to the left", ->
|
||||
pane = atom.workspace.getActivePane()
|
||||
|
|
@ -384,6 +443,15 @@ describe "the commands", ->
|
|||
# FIXME: Should test whether the new pane contains a TextEditor
|
||||
# pointing to the same path
|
||||
|
||||
describe ":vnew", ->
|
||||
it "splits a new file to the left", ->
|
||||
pane = atom.workspace.getActivePane()
|
||||
spyOn(pane, 'splitLeft').andCallThrough()
|
||||
keydown(':')
|
||||
submitNormalModeInputText('vnew')
|
||||
expect(pane.splitLeft).toHaveBeenCalled()
|
||||
# FIXME: Should test whether the new pane contains an empty file
|
||||
|
||||
describe ":delete", ->
|
||||
beforeEach ->
|
||||
editor.setText('abc\ndef\nghi\njkl')
|
||||
|
|
@ -449,13 +517,89 @@ describe "the commands", ->
|
|||
submitNormalModeInputText(':%substitute/abc/ghi/ig')
|
||||
expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nghiaghi')
|
||||
|
||||
it "can't be delimited by letters", ->
|
||||
it "can't be delimited by letters or \\", ->
|
||||
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')
|
||||
|
||||
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||
submitNormalModeInputText(':substitute\\a\\x\\gi')
|
||||
expect(atom.notifications.notifications[1].message).toEqual(
|
||||
"Command error: Regular expressions can't be delimited by \\")
|
||||
expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC')
|
||||
|
||||
describe "case sensitivity", ->
|
||||
describe "respects the smartcase setting", ->
|
||||
beforeEach ->
|
||||
editor.setText('abcaABC\ndefdDEF\nabcaABC')
|
||||
|
||||
it "uses case sensitive search if smartcase is off and the pattern is lowercase", ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', false)
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':substitute/abc/ghi/g')
|
||||
expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nabcaABC')
|
||||
|
||||
it "uses case sensitive search if smartcase is off and the pattern is uppercase", ->
|
||||
editor.setText('abcaABC\ndefdDEF\nabcaABC')
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':substitute/ABC/ghi/g')
|
||||
expect(editor.getText()).toEqual('abcaghi\ndefdDEF\nabcaABC')
|
||||
|
||||
it "uses case insensitive search if smartcase is on and the pattern is lowercase", ->
|
||||
editor.setText('abcaABC\ndefdDEF\nabcaABC')
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', true)
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':substitute/abc/ghi/g')
|
||||
expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC')
|
||||
|
||||
it "uses case sensitive search if smartcase is on and the pattern is uppercase", ->
|
||||
editor.setText('abcaABC\ndefdDEF\nabcaABC')
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':substitute/ABC/ghi/g')
|
||||
expect(editor.getText()).toEqual('abcaghi\ndefdDEF\nabcaABC')
|
||||
|
||||
describe "\\c and \\C in the pattern", ->
|
||||
beforeEach ->
|
||||
editor.setText('abcaABC\ndefdDEF\nabcaABC')
|
||||
|
||||
it "uses case insensitive search if smartcase is off and \c is in the pattern", ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', false)
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':substitute/abc\\c/ghi/g')
|
||||
expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC')
|
||||
|
||||
it "doesn't matter where in the pattern \\c is", ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', false)
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':substitute/a\\cbc/ghi/g')
|
||||
expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC')
|
||||
|
||||
it "uses case sensitive search if smartcase is on, \\C is in the pattern and the pattern is lowercase", ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', true)
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':substitute/a\\Cbc/ghi/g')
|
||||
expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nabcaABC')
|
||||
|
||||
it "overrides \\C with \\c if \\C comes first", ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', true)
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':substitute/a\\Cb\\cc/ghi/g')
|
||||
expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC')
|
||||
|
||||
it "overrides \\C with \\c if \\c comes first", ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', true)
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':substitute/a\\cb\\Cc/ghi/g')
|
||||
expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nabcaABC')
|
||||
|
||||
it "overrides an appended /i flag with \\C", ->
|
||||
atom.config.set('vim-mode.useSmartcaseForSearch', true)
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':substitute/ab\\Cc/ghi/gi')
|
||||
expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nabcaABC')
|
||||
|
||||
describe "capturing groups", ->
|
||||
beforeEach ->
|
||||
editor.setText('abcaABC\ndefdDEF\nabcaABC')
|
||||
|
|
@ -478,7 +622,7 @@ describe "the commands", ->
|
|||
describe ":set", ->
|
||||
it "throws an error without a specified option", ->
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':set')
|
||||
submitNormalModeInputText('set')
|
||||
expect(atom.notifications.notifications[0].message).toEqual(
|
||||
'Command error: No option specified')
|
||||
|
||||
|
|
@ -486,33 +630,47 @@ describe "the commands", ->
|
|||
atom.config.set('editor.showInvisibles', false)
|
||||
atom.config.set('editor.showLineNumbers', false)
|
||||
keydown(':')
|
||||
submitNormalModeInputText(':set list number')
|
||||
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 options to false with no{option}", ->
|
||||
atom.config.set('editor.showInvisibles', true)
|
||||
keydown(':')
|
||||
submitNormalModeInputText('set nolist')
|
||||
expect(atom.config.get('editor.showInvisibles')).toBe(false)
|
||||
|
||||
it "sets (no)list", ->
|
||||
it "inverts options with inv{option}", ->
|
||||
atom.config.set('editor.showInvisibles', true)
|
||||
keydown(':')
|
||||
submitNormalModeInputText('set invlist')
|
||||
expect(atom.config.get('editor.showInvisibles')).toBe(false)
|
||||
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||
submitNormalModeInputText('set invlist')
|
||||
expect(atom.config.get('editor.showInvisibles')).toBe(true)
|
||||
|
||||
it "inverts options with {option}!", ->
|
||||
atom.config.set('editor.showInvisibles', true)
|
||||
keydown(':')
|
||||
submitNormalModeInputText('set list!')
|
||||
expect(atom.config.get('editor.showInvisibles')).toBe(false)
|
||||
atom.commands.dispatch(editorElement, 'ex-mode:open')
|
||||
submitNormalModeInputText('set list!')
|
||||
expect(atom.config.get('editor.showInvisibles')).toBe(true)
|
||||
|
||||
describe "the options", ->
|
||||
it "sets list", ->
|
||||
atom.config.set('editor.showInvisibles', false)
|
||||
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)", ->
|
||||
it "sets nu[mber]", ->
|
||||
atom.config.set('editor.showLineNumbers', false)
|
||||
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.config.set('editor.showLineNumbers', 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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue