Merge pull request #118 from jazzpi/rework-commands

Rework command calling, improve :substitute
This commit is contained in:
jazzpi 2015-12-08 13:13:11 +01:00
commit 19f1a74812
3 changed files with 239 additions and 79 deletions

View file

@ -150,7 +150,7 @@ class Command
# If the command matches an existing one exactly, execute that one # If the command matches an existing one exactly, execute that one
if (func = Ex.singleton()[command])? if (func = Ex.singleton()[command])?
func(range, args) func({ range, args, @vimState, @exState, @editor })
else else
# Step 8: Match command against existing commands # Step 8: Match command against existing commands
matching = (name for name, val of Ex.singleton() when \ matching = (name for name, val of Ex.singleton() when \
@ -162,7 +162,7 @@ class Command
func = Ex.singleton()[command] func = Ex.singleton()[command]
if func? if func?
func(range, args) func({ range, args, @vimState, @exState, @editor })
else else
throw new CommandError("Not an editor command: #{input.characters}") throw new CommandError("Not an editor command: #{input.characters}")

View file

@ -2,6 +2,7 @@ path = require 'path'
CommandError = require './command-error' CommandError = require './command-error'
fs = require 'fs-plus' fs = require 'fs-plus'
VimOption = require './vim-option' VimOption = require './vim-option'
_ = require 'underscore-plus'
trySave = (func) -> trySave = (func) ->
deferred = Promise.defer() deferred = Promise.defer()
@ -32,8 +33,7 @@ trySave = (func) ->
deferred.promise deferred.promise
saveAs = (filePath) -> saveAs = (filePath, editor) ->
editor = atom.workspace.getActiveTextEditor()
fs.writeFileSync(filePath, editor.getText()) fs.writeFileSync(filePath, editor.getText())
getFullPath = (filePath) -> getFullPath = (filePath) ->
@ -64,6 +64,41 @@ replaceGroups = (groups, string) ->
replaced replaced
getSearchTerm = (term, modifiers = {'g': true}) ->
escaped = false
hasc = false
hasC = false
term_ = term
term = ''
for char in term_
if char is '\\' and not escaped
escaped = true
term += char
else
if char is 'c' and escaped
hasc = true
term = term[...-1]
else if char is 'C' and escaped
hasC = true
term = term[...-1]
else if char isnt '\\'
term += char
escaped = false
if hasC
modifiers['i'] = false
if (not hasC and not term.match('[A-Z]') and \
atom.config.get('vim-mode.useSmartcaseForSearch')) or hasc
modifiers['i'] = true
modFlags = Object.keys(modifiers).filter((key) -> modifiers[key]).join('')
try
new RegExp(term, modFlags)
catch
new RegExp(_.escapeRegExp(term), modFlags)
class Ex class Ex
@singleton: => @singleton: =>
@ex ||= new Ex @ex ||= new Ex
@ -76,21 +111,21 @@ class Ex
q: => @quit() q: => @quit()
tabedit: (range, args) => tabedit: (args) =>
if args.trim() isnt '' if args.args.trim() isnt ''
@edit(range, args) @edit(args)
else else
@tabnew(range, args) @tabnew(args)
tabe: (args...) => @tabedit(args...) tabe: (args) => @tabedit(args)
tabnew: (range, args) => tabnew: ({ range, args }) =>
if args.trim() is '' if args.trim() is ''
atom.workspace.open() atom.workspace.open()
else else
@tabedit(range, args) @tabedit(range, args)
tabclose: (args...) => @quit(args...) tabclose: (args) => @quit(args)
tabc: => @tabclose() tabc: => @tabclose()
@ -106,15 +141,14 @@ class Ex
tabp: => @tabprevious() tabp: => @tabprevious()
edit: (range, filePath) -> edit: ({ range, args, editor }) ->
filePath = filePath.trim() filePath = args.trim()
if filePath[0] is '!' if filePath[0] is '!'
force = true force = true
filePath = filePath[1..].trim() filePath = filePath[1..].trim()
else else
force = false force = false
editor = atom.workspace.getActiveTextEditor()
if editor.isModified() and not force if editor.isModified() and not force
throw new CommandError('No write since last change (add ! to override)') throw new CommandError('No write since last change (add ! to override)')
if filePath.indexOf(' ') isnt -1 if filePath.indexOf(' ') isnt -1
@ -132,14 +166,16 @@ class Ex
else else
throw new CommandError('No file name') throw new CommandError('No file name')
e: (args...) => @edit(args...) e: (args) => @edit(args)
enew: -> enew: ->
buffer = atom.workspace.getActiveTextEditor().buffer buffer = atom.workspace.getActiveTextEditor().buffer
buffer.setPath(undefined) buffer.setPath(undefined)
buffer.load() buffer.load()
write: (range, filePath, saveas = false) -> write: ({ range, args, editor, saveas }) ->
saveas ?= false
filePath = args
if filePath[0] is '!' if filePath[0] is '!'
force = true force = true
filePath = filePath[1..] filePath = filePath[1..]
@ -171,27 +207,28 @@ class Ex
throw new CommandError("File exists (add ! to override)") throw new CommandError("File exists (add ! to override)")
if saveas if saveas
editor = atom.workspace.getActiveTextEditor() editor = atom.workspace.getActiveTextEditor()
trySave(-> editor.saveAs(fullPath)).then(deferred.resolve) trySave(-> editor.saveAs(fullPath, editor)).then(deferred.resolve)
else else
trySave(-> saveAs(fullPath)).then(deferred.resolve) trySave(-> saveAs(fullPath, editor)).then(deferred.resolve)
deferred.promise deferred.promise
w: (args...) => w: (args) =>
@write(args...) @write(args)
wq: (args...) => wq: (args) =>
@write(args...).then => @quit() @write(args).then => @quit()
saveas: (range, filePath) => saveas: (args) =>
@write(range, filePath, true) args.saveas = true
@write(args)
xit: (args...) => @wq(args...) xit: (args) => @wq(args)
wa: -> wa: ->
atom.workspace.saveAll() atom.workspace.saveAll()
split: (range, args) -> split: ({ range, args }) ->
args = args.trim() args = args.trim()
filePaths = args.split(' ') filePaths = args.split(' ')
filePaths = undefined if filePaths.length is 1 and filePaths[0] is '' filePaths = undefined if filePaths.length is 1 and filePaths[0] is ''
@ -204,55 +241,71 @@ class Ex
else else
pane.splitUp(copyActiveItem: true) pane.splitUp(copyActiveItem: true)
sp: (args...) => @split(args...) sp: (args) => @split(args)
substitute: (range, args) -> substitute: ({ range, args, editor, vimState }) ->
args = args.trimLeft() args_ = args.trimLeft()
delim = args[0] delim = args_[0]
if /[a-z1-9\\"|]/i.test(delim) if /[a-z1-9\\"|]/i.test(delim)
throw new CommandError( throw new CommandError(
"Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") "Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'")
delimRE = new RegExp("[^\\\\]#{delim}") args_ = args_[1..]
spl = [] escapeChars = {t: '\t', n: '\n', r: '\r'}
args_ = args[1..] parsed = ['', '', '']
while (i = args_.search(delimRE)) isnt -1 parsing = 0
spl.push args_[..i] escaped = false
args_ = args_[i + 2..] while (char = args_[0])?
if args_.length is 0 and spl.length is 3 args_ = args_[1..]
if char is delim
if not escaped
parsing++
if parsing > 2
throw new CommandError('Trailing characters') throw new CommandError('Trailing characters')
else if args_.length isnt 0 else
spl.push args_ parsed[parsing] = parsed[parsing][...-1]
if spl.length > 3 else if char is '\\' and not escaped
throw new CommandError('Trailing characters') parsed[parsing] += char
spl[1] ?= '' escaped = true
spl[2] ?= '' else if parsing == 1 and escaped and escapeChars[char]?
notDelimRE = new RegExp("\\\\#{delim}", 'g') parsed[parsing] += escapeChars[char]
spl[0] = spl[0].replace(notDelimRE, delim) escaped = false
spl[1] = spl[1].replace(notDelimRE, delim) 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 try
pattern = new RegExp(spl[0], spl[2]) flagsObj = {}
flags.split('').forEach((flag) -> flagsObj[flag] = true)
patternRE = getSearchTerm(pattern, flagsObj)
catch e catch e
if e.message.indexOf('Invalid flags supplied to RegExp constructor') is 0 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..]}") throw new CommandError("Invalid flags: #{e.message[45..]}")
else if e.message.indexOf('Invalid regular expression: ') is 0 else if e.message.indexOf('Invalid regular expression: ') is 0
throw new CommandError("Invalid RegEx: #{e.message[27..]}") throw new CommandError("Invalid RegEx: #{e.message[27..]}")
else else
throw e throw e
buffer = atom.workspace.getActiveTextEditor().buffer editor.transact ->
atom.workspace.getActiveTextEditor().transact ->
for line in [range[0]..range[1]] for line in [range[0]..range[1]]
buffer.scanInRange(pattern, editor.scanInBufferRange(
[[line, 0], [line, buffer.lines[line].length]], patternRE,
({match, matchText, range, stop, replace}) -> [[line, 0], [line + 1, 0]],
replace(replaceGroups(match[..], spl[1])) ({match, replace}) ->
replace(replaceGroups(match[..], substition))
) )
s: (args...) => @substitute(args...) s: (args) => @substitute(args)
vsplit: (range, args) -> vsplit: ({ range, args }) ->
args = args.trim() args = args.trim()
filePaths = args.split(' ') filePaths = args.split(' ')
filePaths = undefined if filePaths.length is 1 and filePaths[0] is '' filePaths = undefined if filePaths.length is 1 and filePaths[0] is ''
@ -265,13 +318,13 @@ class Ex
else else
pane.splitLeft(copyActiveItem: true) pane.splitLeft(copyActiveItem: true)
vsp: (args...) => @vsplit(args...) vsp: (args) => @vsplit(args)
delete: (range) -> delete: ({ range }) ->
range = [[range[0], 0], [range[1] + 1, 0]] range = [[range[0], 0], [range[1] + 1, 0]]
atom.workspace.getActiveTextEditor().buffer.setTextInRange(range, '') atom.workspace.getActiveTextEditor().buffer.setTextInRange(range, '')
set: (range, args) -> set: ({ range, args }) ->
args = args.trim() args = args.trim()
if args == "" if args == ""
throw new CommandError("No option specified") throw new CommandError("No option specified")

View file

@ -12,11 +12,21 @@ describe "the commands", ->
beforeEach -> beforeEach ->
vimMode = atom.packages.loadPackage('vim-mode') vimMode = atom.packages.loadPackage('vim-mode')
exMode = atom.packages.loadPackage('ex-mode') exMode = atom.packages.loadPackage('ex-mode')
exMode.activate() waitsForPromise ->
activationPromise = exMode.activate()
helpers.activateExMode()
activationPromise
runs ->
spyOn(exMode.mainModule.globalExState, 'setVim').andCallThrough()
waitsForPromise -> waitsForPromise ->
vimMode.activate().then -> vimMode.activate()
helpers.activateExMode().then ->
waitsFor ->
exMode.mainModule.globalExState.setVim.calls.length > 0
runs ->
dir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") dir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}")
dir2 = 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(dir)
@ -24,7 +34,7 @@ describe "the commands", ->
atom.project.setPaths([dir, dir2]) atom.project.setPaths([dir, dir2])
helpers.getEditorElement (element) -> helpers.getEditorElement (element) ->
atom.commands.dispatch(element, 'ex-mode:open') atom.commands.dispatch(element, "ex-mode:open")
keydown('escape') keydown('escape')
editorElement = element editorElement = element
editor = editorElement.getModel() editor = editorElement.getModel()
@ -355,7 +365,7 @@ describe "the commands", ->
submitNormalModeInputText('wq wq-2') submitNormalModeInputText('wq wq-2')
expect(Ex.write) expect(Ex.write)
.toHaveBeenCalled() .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) waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100)
describe ":xit", -> describe ":xit", ->
@ -565,6 +575,103 @@ describe "the commands", ->
it "can't be delimited by '\"'", -> test '"' it "can't be delimited by '\"'", -> test '"'
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", -> describe "capturing groups", ->
beforeEach -> beforeEach ->
editor.setText('abcaABC\ndefdDEF\nabcaABC') editor.setText('abcaABC\ndefdDEF\nabcaABC')