Merge pull request #118 from jazzpi/rework-commands
Rework command calling, improve :substitute
This commit is contained in:
commit
19f1a74812
3 changed files with 239 additions and 79 deletions
|
|
@ -150,7 +150,7 @@ class Command
|
|||
|
||||
# If the command matches an existing one exactly, execute that one
|
||||
if (func = Ex.singleton()[command])?
|
||||
func(range, args)
|
||||
func({ range, args, @vimState, @exState, @editor })
|
||||
else
|
||||
# Step 8: Match command against existing commands
|
||||
matching = (name for name, val of Ex.singleton() when \
|
||||
|
|
@ -162,7 +162,7 @@ class Command
|
|||
|
||||
func = Ex.singleton()[command]
|
||||
if func?
|
||||
func(range, args)
|
||||
func({ range, args, @vimState, @exState, @editor })
|
||||
else
|
||||
throw new CommandError("Not an editor command: #{input.characters}")
|
||||
|
||||
|
|
|
|||
167
lib/ex.coffee
167
lib/ex.coffee
|
|
@ -2,6 +2,7 @@ path = require 'path'
|
|||
CommandError = require './command-error'
|
||||
fs = require 'fs-plus'
|
||||
VimOption = require './vim-option'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
trySave = (func) ->
|
||||
deferred = Promise.defer()
|
||||
|
|
@ -32,8 +33,7 @@ trySave = (func) ->
|
|||
|
||||
deferred.promise
|
||||
|
||||
saveAs = (filePath) ->
|
||||
editor = atom.workspace.getActiveTextEditor()
|
||||
saveAs = (filePath, editor) ->
|
||||
fs.writeFileSync(filePath, editor.getText())
|
||||
|
||||
getFullPath = (filePath) ->
|
||||
|
|
@ -64,6 +64,41 @@ replaceGroups = (groups, string) ->
|
|||
|
||||
replaced
|
||||
|
||||
getSearchTerm = (term, modifiers = {'g': true}) ->
|
||||
|
||||
escaped = false
|
||||
hasc = false
|
||||
hasC = false
|
||||
term_ = term
|
||||
term = ''
|
||||
for char in term_
|
||||
if char is '\\' and not escaped
|
||||
escaped = true
|
||||
term += char
|
||||
else
|
||||
if char is 'c' and escaped
|
||||
hasc = true
|
||||
term = term[...-1]
|
||||
else if char is 'C' and escaped
|
||||
hasC = true
|
||||
term = term[...-1]
|
||||
else if char isnt '\\'
|
||||
term += char
|
||||
escaped = false
|
||||
|
||||
if hasC
|
||||
modifiers['i'] = false
|
||||
if (not hasC and not term.match('[A-Z]') and \
|
||||
atom.config.get('vim-mode.useSmartcaseForSearch')) or hasc
|
||||
modifiers['i'] = true
|
||||
|
||||
modFlags = Object.keys(modifiers).filter((key) -> modifiers[key]).join('')
|
||||
|
||||
try
|
||||
new RegExp(term, modFlags)
|
||||
catch
|
||||
new RegExp(_.escapeRegExp(term), modFlags)
|
||||
|
||||
class Ex
|
||||
@singleton: =>
|
||||
@ex ||= new Ex
|
||||
|
|
@ -76,21 +111,21 @@ class Ex
|
|||
|
||||
q: => @quit()
|
||||
|
||||
tabedit: (range, args) =>
|
||||
if args.trim() isnt ''
|
||||
@edit(range, args)
|
||||
tabedit: (args) =>
|
||||
if args.args.trim() isnt ''
|
||||
@edit(args)
|
||||
else
|
||||
@tabnew(range, args)
|
||||
@tabnew(args)
|
||||
|
||||
tabe: (args...) => @tabedit(args...)
|
||||
tabe: (args) => @tabedit(args)
|
||||
|
||||
tabnew: (range, args) =>
|
||||
tabnew: ({ range, args }) =>
|
||||
if args.trim() is ''
|
||||
atom.workspace.open()
|
||||
else
|
||||
@tabedit(range, args)
|
||||
|
||||
tabclose: (args...) => @quit(args...)
|
||||
tabclose: (args) => @quit(args)
|
||||
|
||||
tabc: => @tabclose()
|
||||
|
||||
|
|
@ -106,15 +141,14 @@ class Ex
|
|||
|
||||
tabp: => @tabprevious()
|
||||
|
||||
edit: (range, filePath) ->
|
||||
filePath = filePath.trim()
|
||||
edit: ({ range, args, editor }) ->
|
||||
filePath = args.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
|
||||
|
|
@ -132,14 +166,16 @@ class Ex
|
|||
else
|
||||
throw new CommandError('No file name')
|
||||
|
||||
e: (args...) => @edit(args...)
|
||||
e: (args) => @edit(args)
|
||||
|
||||
enew: ->
|
||||
buffer = atom.workspace.getActiveTextEditor().buffer
|
||||
buffer.setPath(undefined)
|
||||
buffer.load()
|
||||
|
||||
write: (range, filePath, saveas = false) ->
|
||||
write: ({ range, args, editor, saveas }) ->
|
||||
saveas ?= false
|
||||
filePath = args
|
||||
if filePath[0] is '!'
|
||||
force = true
|
||||
filePath = filePath[1..]
|
||||
|
|
@ -171,27 +207,28 @@ class Ex
|
|||
throw new CommandError("File exists (add ! to override)")
|
||||
if saveas
|
||||
editor = atom.workspace.getActiveTextEditor()
|
||||
trySave(-> editor.saveAs(fullPath)).then(deferred.resolve)
|
||||
trySave(-> editor.saveAs(fullPath, editor)).then(deferred.resolve)
|
||||
else
|
||||
trySave(-> saveAs(fullPath)).then(deferred.resolve)
|
||||
trySave(-> saveAs(fullPath, editor)).then(deferred.resolve)
|
||||
|
||||
deferred.promise
|
||||
|
||||
w: (args...) =>
|
||||
@write(args...)
|
||||
w: (args) =>
|
||||
@write(args)
|
||||
|
||||
wq: (args...) =>
|
||||
@write(args...).then => @quit()
|
||||
wq: (args) =>
|
||||
@write(args).then => @quit()
|
||||
|
||||
saveas: (range, filePath) =>
|
||||
@write(range, filePath, true)
|
||||
saveas: (args) =>
|
||||
args.saveas = true
|
||||
@write(args)
|
||||
|
||||
xit: (args...) => @wq(args...)
|
||||
xit: (args) => @wq(args)
|
||||
|
||||
wa: ->
|
||||
atom.workspace.saveAll()
|
||||
|
||||
split: (range, args) ->
|
||||
split: ({ range, args }) ->
|
||||
args = args.trim()
|
||||
filePaths = args.split(' ')
|
||||
filePaths = undefined if filePaths.length is 1 and filePaths[0] is ''
|
||||
|
|
@ -204,55 +241,71 @@ class Ex
|
|||
else
|
||||
pane.splitUp(copyActiveItem: true)
|
||||
|
||||
sp: (args...) => @split(args...)
|
||||
sp: (args) => @split(args)
|
||||
|
||||
substitute: (range, args) ->
|
||||
args = args.trimLeft()
|
||||
delim = args[0]
|
||||
substitute: ({ range, args, editor, vimState }) ->
|
||||
args_ = args.trimLeft()
|
||||
delim = args_[0]
|
||||
if /[a-z1-9\\"|]/i.test(delim)
|
||||
throw new CommandError(
|
||||
"Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'")
|
||||
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
|
||||
args_ = args_[1..]
|
||||
escapeChars = {t: '\t', n: '\n', r: '\r'}
|
||||
parsed = ['', '', '']
|
||||
parsing = 0
|
||||
escaped = false
|
||||
while (char = args_[0])?
|
||||
args_ = args_[1..]
|
||||
if char is delim
|
||||
if not escaped
|
||||
parsing++
|
||||
if parsing > 2
|
||||
throw new CommandError('Trailing characters')
|
||||
else 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)
|
||||
else
|
||||
parsed[parsing] = parsed[parsing][...-1]
|
||||
else if char is '\\' and not escaped
|
||||
parsed[parsing] += char
|
||||
escaped = true
|
||||
else if parsing == 1 and escaped and escapeChars[char]?
|
||||
parsed[parsing] += escapeChars[char]
|
||||
escaped = false
|
||||
else
|
||||
escaped = false
|
||||
parsed[parsing] += char
|
||||
|
||||
[pattern, substition, flags] = parsed
|
||||
if pattern is ''
|
||||
pattern = vimState.getSearchHistoryItem()
|
||||
if not pattern?
|
||||
atom.beep()
|
||||
throw new CommandError('No previous regular expression')
|
||||
else
|
||||
vimState.pushSearchHistory(pattern)
|
||||
|
||||
try
|
||||
pattern = new RegExp(spl[0], spl[2])
|
||||
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
|
||||
# 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 ->
|
||||
editor.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]))
|
||||
editor.scanInBufferRange(
|
||||
patternRE,
|
||||
[[line, 0], [line + 1, 0]],
|
||||
({match, replace}) ->
|
||||
replace(replaceGroups(match[..], substition))
|
||||
)
|
||||
|
||||
s: (args...) => @substitute(args...)
|
||||
s: (args) => @substitute(args)
|
||||
|
||||
vsplit: (range, args) ->
|
||||
vsplit: ({ range, args }) ->
|
||||
args = args.trim()
|
||||
filePaths = args.split(' ')
|
||||
filePaths = undefined if filePaths.length is 1 and filePaths[0] is ''
|
||||
|
|
@ -265,13 +318,13 @@ class Ex
|
|||
else
|
||||
pane.splitLeft(copyActiveItem: true)
|
||||
|
||||
vsp: (args...) => @vsplit(args...)
|
||||
vsp: (args) => @vsplit(args)
|
||||
|
||||
delete: (range) ->
|
||||
delete: ({ range }) ->
|
||||
range = [[range[0], 0], [range[1] + 1, 0]]
|
||||
atom.workspace.getActiveTextEditor().buffer.setTextInRange(range, '')
|
||||
|
||||
set: (range, args) ->
|
||||
set: ({ range, args }) ->
|
||||
args = args.trim()
|
||||
if args == ""
|
||||
throw new CommandError("No option specified")
|
||||
|
|
|
|||
|
|
@ -12,11 +12,21 @@ describe "the commands", ->
|
|||
beforeEach ->
|
||||
vimMode = atom.packages.loadPackage('vim-mode')
|
||||
exMode = atom.packages.loadPackage('ex-mode')
|
||||
exMode.activate()
|
||||
waitsForPromise ->
|
||||
activationPromise = exMode.activate()
|
||||
helpers.activateExMode()
|
||||
activationPromise
|
||||
|
||||
runs ->
|
||||
spyOn(exMode.mainModule.globalExState, 'setVim').andCallThrough()
|
||||
|
||||
waitsForPromise ->
|
||||
vimMode.activate().then ->
|
||||
helpers.activateExMode().then ->
|
||||
vimMode.activate()
|
||||
|
||||
waitsFor ->
|
||||
exMode.mainModule.globalExState.setVim.calls.length > 0
|
||||
|
||||
runs ->
|
||||
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)
|
||||
|
|
@ -24,7 +34,7 @@ describe "the commands", ->
|
|||
atom.project.setPaths([dir, dir2])
|
||||
|
||||
helpers.getEditorElement (element) ->
|
||||
atom.commands.dispatch(element, 'ex-mode:open')
|
||||
atom.commands.dispatch(element, "ex-mode:open")
|
||||
keydown('escape')
|
||||
editorElement = element
|
||||
editor = editorElement.getModel()
|
||||
|
|
@ -355,7 +365,7 @@ describe "the commands", ->
|
|||
submitNormalModeInputText('wq wq-2')
|
||||
expect(Ex.write)
|
||||
.toHaveBeenCalled()
|
||||
expect(Ex.write.calls[0].args[1].trim()).toEqual('wq-2')
|
||||
expect(Ex.write.calls[0].args[0].args.trim()).toEqual('wq-2')
|
||||
waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100)
|
||||
|
||||
describe ":xit", ->
|
||||
|
|
@ -565,6 +575,103 @@ describe "the commands", ->
|
|||
it "can't be delimited by '\"'", -> test '"'
|
||||
it "can't be delimited by '|'", -> test '|'
|
||||
|
||||
describe "empty replacement", ->
|
||||
beforeEach ->
|
||||
editor.setText('abcabc\nabcabc')
|
||||
|
||||
it "removes the pattern without modifiers", ->
|
||||
keydown(':')
|
||||
submitNormalModeInputText(":substitute/abc//")
|
||||
expect(editor.getText()).toEqual('abc\nabcabc')
|
||||
|
||||
it "removes the pattern with modifiers", ->
|
||||
keydown(':')
|
||||
submitNormalModeInputText(":substitute/abc//g")
|
||||
expect(editor.getText()).toEqual('\nabcabc')
|
||||
|
||||
describe "replacing with escape sequences", ->
|
||||
beforeEach ->
|
||||
editor.setText('abc,def,ghi')
|
||||
|
||||
test = (escapeChar, escaped) ->
|
||||
keydown(':')
|
||||
submitNormalModeInputText(":substitute/,/\\#{escapeChar}/g")
|
||||
expect(editor.getText()).toEqual("abc#{escaped}def#{escaped}ghi")
|
||||
|
||||
it "replaces with a tab", -> test('t', '\t')
|
||||
it "replaces with a linefeed", -> test('n', '\n')
|
||||
it "replaces with a carriage return", -> test('r', '\r')
|
||||
|
||||
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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue