New command defining format, minor improvements

- If the second address is empty, it is assumed to be `.`
- Regex addresses and `:substitute` now integrate with search history and
  respect case sensitivity settings
- Patterns for `:substitute` can't be delimited by
- `:set` now supports inverting options using `:set inv{option}` and
  `:set {option}!`
- New commands: `:new`, `:vnew`, `:exit`, `:xall`, `:wall`, `:qall`, `:update`
This commit is contained in:
jazzpi 2015-07-31 12:01:47 +02:00
parent 962e4a35ba
commit 91f3f82730
10 changed files with 862 additions and 400 deletions

321
lib/ex-commands.coffee Normal file
View 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)