Improve :substitute
Rework the parsing algorithm so that it works (mostly) without using RegEx's. This allows for replacing with an empty string and escape sequences (\t, \n, \r). Fixes #71, #93, #117
This commit is contained in:
parent
af0ba7c01c
commit
ddbdb861fb
2 changed files with 176 additions and 27 deletions
104
lib/ex.coffee
104
lib/ex.coffee
|
|
@ -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()
|
||||||
|
|
@ -63,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
|
||||||
|
|
@ -196,47 +232,63 @@ class Ex
|
||||||
sp: (args) => @split(args)
|
sp: (args) => @split(args)
|
||||||
|
|
||||||
substitute: ({ range, args, editor, vimState }) ->
|
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)
|
||||||
|
|
|
||||||
|
|
@ -473,6 +473,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')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue