Merge pull request #26 from jazzpi/patch-3
Implement parsing following ex spec and :s, fix behaviour of :e
This commit is contained in:
commit
4da6f2d987
7 changed files with 310 additions and 48 deletions
5
lib/command-error.coffee
Normal file
5
lib/command-error.coffee
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
class CommandError
|
||||||
|
constructor: (@message) ->
|
||||||
|
@name = 'Command Error'
|
||||||
|
|
||||||
|
module.exports = CommandError
|
||||||
|
|
@ -1,22 +1,164 @@
|
||||||
ExViewModel = require './ex-view-model'
|
ExViewModel = require './ex-view-model'
|
||||||
Ex = require './ex'
|
Ex = require './ex'
|
||||||
|
Find = require './find'
|
||||||
class CommandError
|
CommandError = require './command-error'
|
||||||
constructor: (@message) ->
|
|
||||||
@name = 'Command Error'
|
|
||||||
|
|
||||||
class Command
|
class Command
|
||||||
constructor: (@editor, @exState) ->
|
constructor: (@editor, @exState) ->
|
||||||
@viewModel = new ExViewModel(@)
|
@viewModel = new ExViewModel(@)
|
||||||
|
|
||||||
execute: (input) ->
|
parseAddr: (str, curPos) ->
|
||||||
return unless input.characters.length > 0
|
if str is '.'
|
||||||
[command, args...] = input.characters.split(" ")
|
addr = curPos.row
|
||||||
|
else if str is '$'
|
||||||
|
# Lines are 0-indexed in Atom, but 1-indexed in vim.
|
||||||
|
addr = @editor.getBuffer().lines.length - 1
|
||||||
|
else if str[0] in ["+", "-"]
|
||||||
|
addr = curPos.row + @parseOffset(str)
|
||||||
|
else if not isNaN(str)
|
||||||
|
addr = parseInt(str) - 1
|
||||||
|
else if str[0] is "'" # Parse Mark...
|
||||||
|
unless @vimState?
|
||||||
|
throw new CommandError("Couldn't get access to vim-mode.")
|
||||||
|
mark = @vimState.marks[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])
|
||||||
|
unless addr?
|
||||||
|
throw new CommandError("Pattern not found: #{str[1...-1]}")
|
||||||
|
|
||||||
func = Ex.singleton()[command]
|
return addr
|
||||||
if func?
|
|
||||||
func(args...)
|
parseOffset: (str) ->
|
||||||
|
if str.length is 0
|
||||||
|
return 0
|
||||||
|
if str.length is 1
|
||||||
|
o = 1
|
||||||
else
|
else
|
||||||
throw new CommandError("#{input.characters}")
|
o = parseInt(str[1..])
|
||||||
|
if str[0] is '+'
|
||||||
|
return o
|
||||||
|
else
|
||||||
|
return -o
|
||||||
|
|
||||||
module.exports = {Command, CommandError}
|
execute: (input) ->
|
||||||
|
@vimState = @exState.globalExState.vim?.getEditorState(@editor)
|
||||||
|
# Command line parsing (mostly) following the rules at
|
||||||
|
# http://pubs.opengroup.org/onlinepubs/9699919799/utilities
|
||||||
|
# /ex.html#tag_20_40_13_03
|
||||||
|
# Steps 1/2: Leading blanks and colons are ignored.
|
||||||
|
cl = input.characters
|
||||||
|
cl = cl.replace(/^(:|\s)*/, '')
|
||||||
|
return unless cl.length > 0
|
||||||
|
# Step 3: If the first character is a ", ignore the rest of the line
|
||||||
|
if cl[0] is '"'
|
||||||
|
return
|
||||||
|
# Step 4: Address parsing
|
||||||
|
lastLine = @editor.getBuffer().lines.length - 1
|
||||||
|
if cl[0] is '%'
|
||||||
|
range = [0, lastLine]
|
||||||
|
cl = cl[1..]
|
||||||
|
else
|
||||||
|
addrPattern = ///^
|
||||||
|
(?: # First address
|
||||||
|
(
|
||||||
|
\.| # Current line
|
||||||
|
\$| # Last line
|
||||||
|
\d+| # n-th line
|
||||||
|
'[\[\]<>'`"^.(){}a-zA-Z]| # Marks
|
||||||
|
/.*?[^\\]/| # Regex
|
||||||
|
\?.*?[^\\]\?| # Backwards search
|
||||||
|
[+-]\d* # Current line +/- a number of lines
|
||||||
|
)((?:\s*[+-]\d*)*) # Line offset
|
||||||
|
)?
|
||||||
|
(?:, # Second address
|
||||||
|
( # Same as first address
|
||||||
|
\.|
|
||||||
|
\$|
|
||||||
|
\d+|
|
||||||
|
'[\[\]<>'`"^.(){}a-zA-Z]|
|
||||||
|
/.*?[^\\]/|
|
||||||
|
\?.*?[^\\]\?|
|
||||||
|
[+-]\d*
|
||||||
|
)((?:\s*[+-]\d*)*)
|
||||||
|
)?
|
||||||
|
///
|
||||||
|
|
||||||
|
[match, addr1, off1, addr2, off2] = cl.match(addrPattern)
|
||||||
|
|
||||||
|
curPos = @editor.getCursorBufferPosition()
|
||||||
|
|
||||||
|
if addr1?
|
||||||
|
address1 = @parseAddr(addr1, curPos)
|
||||||
|
else
|
||||||
|
# If no addr1 is given (,+3), assume it is '.'
|
||||||
|
address1 = curPos.row
|
||||||
|
if off1?
|
||||||
|
address1 += @parseOffset(off1)
|
||||||
|
|
||||||
|
if address1 < 0 or address1 > lastLine
|
||||||
|
throw new CommandError('Invalid range')
|
||||||
|
|
||||||
|
if addr2?
|
||||||
|
address2 = @parseAddr(addr2, curPos)
|
||||||
|
if off2?
|
||||||
|
address2 += @parseOffset(off2)
|
||||||
|
|
||||||
|
if address2 < 0 or address2 > lastLine
|
||||||
|
throw new CommandError('Invalid range')
|
||||||
|
|
||||||
|
if address2 < address1
|
||||||
|
throw new CommandError('Backwards range given')
|
||||||
|
|
||||||
|
range = [address1, if address2? then address2 else address1]
|
||||||
|
cl = cl[match?.length..]
|
||||||
|
|
||||||
|
# Step 5: Leading blanks are ignored
|
||||||
|
cl = cl.trimLeft()
|
||||||
|
|
||||||
|
# Step 6a: If no command is specified, go to the last specified address
|
||||||
|
if cl.length is 0
|
||||||
|
@editor.setCursorBufferPosition([range[1], 0])
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ignore steps 6b and 6c since they only make sense for print commands and
|
||||||
|
# print doesn't make sense
|
||||||
|
|
||||||
|
# Ignore step 7a since flags are only useful for print
|
||||||
|
|
||||||
|
# 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])
|
||||||
|
command = 'mark'
|
||||||
|
args = cl[1]
|
||||||
|
else if not /[a-z]/i.test(cl[0])
|
||||||
|
command = cl[0]
|
||||||
|
args = cl[1..]
|
||||||
|
else
|
||||||
|
[m, command, args] = cl.match(/^(\w+)(.*)/)
|
||||||
|
|
||||||
|
# If the command matches an existing one exactly, execute that one
|
||||||
|
if (func = Ex.singleton()[command])?
|
||||||
|
func(range, args)
|
||||||
|
else
|
||||||
|
# Step 8: Match command against existing commands
|
||||||
|
matching = (name for name, val of Ex.singleton() when \
|
||||||
|
name.indexOf(command) is 0)
|
||||||
|
|
||||||
|
matching.sort()
|
||||||
|
|
||||||
|
command = matching[0]
|
||||||
|
|
||||||
|
func = Ex.singleton()[command]
|
||||||
|
if func?
|
||||||
|
func(range, args)
|
||||||
|
else
|
||||||
|
throw new CommandError("Not an editor command: #{input.characters}")
|
||||||
|
|
||||||
|
module.exports = Command
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,4 @@ module.exports = ExMode =
|
||||||
|
|
||||||
consumeVim: (vim) ->
|
consumeVim: (vim) ->
|
||||||
@vim = vim
|
@vim = vim
|
||||||
|
@globalExState.setVim(vim)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
|
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
|
||||||
|
|
||||||
{Command, CommandError} = require './command'
|
Command = require './command'
|
||||||
|
CommandError = require './command-error'
|
||||||
|
|
||||||
class ExState
|
class ExState
|
||||||
constructor: (@editorElement, @globalExState) ->
|
constructor: (@editorElement, @globalExState) ->
|
||||||
|
|
|
||||||
153
lib/ex.coffee
153
lib/ex.coffee
|
|
@ -1,4 +1,5 @@
|
||||||
path = require 'path'
|
path = require 'path'
|
||||||
|
CommandError = require './command-error'
|
||||||
|
|
||||||
trySave = (func) ->
|
trySave = (func) ->
|
||||||
deferred = Promise.defer()
|
deferred = Promise.defer()
|
||||||
|
|
@ -9,20 +10,47 @@ trySave = (func) ->
|
||||||
catch error
|
catch error
|
||||||
if error.message.endsWith('is a directory')
|
if error.message.endsWith('is a directory')
|
||||||
atom.notifications.addWarning("Unable to save file: #{error.message}")
|
atom.notifications.addWarning("Unable to save file: #{error.message}")
|
||||||
else if error.code is 'EACCES' and error.path?
|
else if error.path?
|
||||||
atom.notifications.addWarning("Unable to save file: Permission denied '#{error.path}'")
|
if error.code is 'EACCES'
|
||||||
else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST'] and error.path?
|
atom.notifications
|
||||||
atom.notifications.addWarning("Unable to save file '#{error.path}'", detail: error.message)
|
.addWarning("Unable to save file: Permission denied '#{error.path}'")
|
||||||
else if error.code is 'EROFS' and error.path?
|
else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST']
|
||||||
atom.notifications.addWarning("Unable to save file: Read-only file system '#{error.path}'")
|
atom.notifications.addWarning("Unable to save file '#{error.path}'",
|
||||||
else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message)
|
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]
|
fileName = errorMatch[1]
|
||||||
atom.notifications.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to")
|
atom.notifications.addWarning("Unable to save file: A directory in the "+
|
||||||
|
"path '#{fileName}' could not be written to")
|
||||||
else
|
else
|
||||||
throw error
|
throw error
|
||||||
|
|
||||||
deferred.promise
|
deferred.promise
|
||||||
|
|
||||||
|
getFullPath = (filePath) ->
|
||||||
|
return filePath if path.isAbsolute(filePath)
|
||||||
|
return path.join(atom.project.getPath(), filePath)
|
||||||
|
|
||||||
|
replaceGroups = (groups, replString) ->
|
||||||
|
arr = replString.split('')
|
||||||
|
offset = 0
|
||||||
|
cdiff = 0
|
||||||
|
|
||||||
|
while (m = replString.match(/(?:[^\\]|^)\\(\d)/))?
|
||||||
|
group = groups[m[1]] or ''
|
||||||
|
i = replString.indexOf(m[0])
|
||||||
|
l = m[0].length
|
||||||
|
replString = replString.slice(i + l)
|
||||||
|
arr[i + offset...i + offset + l] = (if l is 2 then '' else m[0][0]) +
|
||||||
|
group
|
||||||
|
arr = arr.join('').split ''
|
||||||
|
offset += i + l - group.length
|
||||||
|
|
||||||
|
return arr.join('').replace(/\\\\(\d)/, '\\$1')
|
||||||
|
|
||||||
class Ex
|
class Ex
|
||||||
@singleton: =>
|
@singleton: =>
|
||||||
@ex ||= new Ex
|
@ex ||= new Ex
|
||||||
|
|
@ -35,7 +63,9 @@ class Ex
|
||||||
|
|
||||||
q: => @quit()
|
q: => @quit()
|
||||||
|
|
||||||
tabedit: (filePaths...) ->
|
tabedit: (range, args) ->
|
||||||
|
args = args.trim()
|
||||||
|
filePaths = args.split(' ')
|
||||||
pane = atom.workspace.getActivePane()
|
pane = atom.workspace.getActivePane()
|
||||||
if filePaths? and filePaths.length > 0
|
if filePaths? and filePaths.length > 0
|
||||||
for file in filePaths
|
for file in filePaths
|
||||||
|
|
@ -43,9 +73,9 @@ class Ex
|
||||||
else
|
else
|
||||||
atom.workspace.openURIInPane('', pane)
|
atom.workspace.openURIInPane('', pane)
|
||||||
|
|
||||||
tabe: (filePaths...) => @tabedit(filePaths...)
|
tabe: (args...) => @tabedit(args...)
|
||||||
|
|
||||||
tabnew: (filePaths...) => @tabedit(filePaths...)
|
tabnew: (args...) => @tabedit(args...)
|
||||||
|
|
||||||
tabclose: => @quit()
|
tabclose: => @quit()
|
||||||
|
|
||||||
|
|
@ -63,25 +93,32 @@ class Ex
|
||||||
|
|
||||||
tabp: => @tabprevious()
|
tabp: => @tabprevious()
|
||||||
|
|
||||||
edit: (filePath) => @tabedit(filePath) if filePath?
|
edit: (range, filePath) ->
|
||||||
|
filePath = filePath.trim()
|
||||||
|
if filePath.indexOf(' ') isnt -1
|
||||||
|
throw new CommandError('Only one file name allowed')
|
||||||
|
buffer = atom.workspace.getActiveEditor().buffer
|
||||||
|
filePath = buffer.getPath() if filePath is ''
|
||||||
|
buffer.setPath(getFullPath(filePath))
|
||||||
|
buffer.load()
|
||||||
|
|
||||||
e: (filePath) => @edit(filePath)
|
e: (args...) => @edit(args...)
|
||||||
|
|
||||||
enew: => @edit()
|
enew: ->
|
||||||
|
buffer = atom.workspace.getActiveEditor().buffer
|
||||||
|
buffer.setPath(undefined)
|
||||||
|
buffer.load()
|
||||||
|
|
||||||
write: (filePath) ->
|
write: (range, filePath) ->
|
||||||
|
filePath = filePath.trim()
|
||||||
deferred = Promise.defer()
|
deferred = Promise.defer()
|
||||||
|
|
||||||
projectPath = atom.project.getPath()
|
|
||||||
pane = atom.workspace.getActivePane()
|
pane = atom.workspace.getActivePane()
|
||||||
editor = atom.workspace.getActiveEditor()
|
editor = atom.workspace.getActiveEditor()
|
||||||
if atom.workspace.getActiveTextEditor().getPath() isnt undefined
|
if atom.workspace.getActiveTextEditor().getPath() isnt undefined
|
||||||
if filePath?
|
if filePath.length > 0
|
||||||
editorPath = editor.getPath()
|
editorPath = editor.getPath()
|
||||||
fullPath = if path.isAbsolute(filePath)
|
fullPath = getFullPath(filePath)
|
||||||
filePath
|
|
||||||
else
|
|
||||||
path.join(projectPath, filePath)
|
|
||||||
trySave(-> editor.saveAs(fullPath))
|
trySave(-> editor.saveAs(fullPath))
|
||||||
.then ->
|
.then ->
|
||||||
deferred.resolve()
|
deferred.resolve()
|
||||||
|
|
@ -90,11 +127,8 @@ class Ex
|
||||||
trySave(-> editor.save())
|
trySave(-> editor.save())
|
||||||
.then deferred.resolve
|
.then deferred.resolve
|
||||||
else
|
else
|
||||||
if filePath?
|
if filePath.length > 0
|
||||||
fullPath = if path.isAbsolute(filePath)
|
fullPath = getFullPath(filePath)
|
||||||
filePath
|
|
||||||
else
|
|
||||||
path.join(projectPath, filePath)
|
|
||||||
trySave(-> editor.saveAs(fullPath))
|
trySave(-> editor.saveAs(fullPath))
|
||||||
.then deferred.resolve
|
.then deferred.resolve
|
||||||
else
|
else
|
||||||
|
|
@ -105,18 +139,22 @@ class Ex
|
||||||
|
|
||||||
deferred.promise
|
deferred.promise
|
||||||
|
|
||||||
w: (filePath) =>
|
w: (args...) =>
|
||||||
@write(filePath)
|
@write(args...)
|
||||||
|
|
||||||
wq: (filePath) =>
|
wq: (args...) =>
|
||||||
@write(filePath).then => @quit()
|
@write(args...).then => @quit()
|
||||||
|
|
||||||
x: => @wq()
|
x: => @wq()
|
||||||
|
|
||||||
wa: ->
|
wa: ->
|
||||||
atom.workspace.saveAll()
|
atom.workspace.saveAll()
|
||||||
|
|
||||||
split: (filePaths...) ->
|
split: (range, args) ->
|
||||||
|
args = args.trim()
|
||||||
|
filePaths = args.split(' ')
|
||||||
|
filePaths = undefined if filePaths.length is 1 and filePaths[0] is ''
|
||||||
|
console.log filePaths, filePaths is ['']
|
||||||
pane = atom.workspace.getActivePane()
|
pane = atom.workspace.getActivePane()
|
||||||
if filePaths? and filePaths.length > 0
|
if filePaths? and filePaths.length > 0
|
||||||
newPane = pane.splitUp()
|
newPane = pane.splitUp()
|
||||||
|
|
@ -126,9 +164,56 @@ class Ex
|
||||||
else
|
else
|
||||||
pane.splitUp(copyActiveItem: true)
|
pane.splitUp(copyActiveItem: true)
|
||||||
|
|
||||||
sp: (filePaths...) => @split(filePaths...)
|
sp: (args...) => @split(args...)
|
||||||
|
|
||||||
vsplit: (filePaths...) ->
|
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] ?= ''
|
||||||
|
|
||||||
|
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
|
||||||
|
cp = buffer.history.createCheckpoint()
|
||||||
|
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]))
|
||||||
|
)
|
||||||
|
buffer.history.groupChangesSinceCheckpoint(cp)
|
||||||
|
|
||||||
|
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()
|
pane = atom.workspace.getActivePane()
|
||||||
if filePaths? and filePaths.length > 0
|
if filePaths? and filePaths.length > 0
|
||||||
newPane = pane.splitLeft()
|
newPane = pane.splitLeft()
|
||||||
|
|
@ -138,6 +223,6 @@ class Ex
|
||||||
else
|
else
|
||||||
pane.splitLeft(copyActiveItem: true)
|
pane.splitLeft(copyActiveItem: true)
|
||||||
|
|
||||||
vsp: (filePaths...) => @vsplit(filePaths...)
|
vsp: (args...) => @vsplit(args...)
|
||||||
|
|
||||||
module.exports = Ex
|
module.exports = Ex
|
||||||
|
|
|
||||||
27
lib/find.coffee
Normal file
27
lib/find.coffee
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
module.exports = {
|
||||||
|
findInBuffer : (buffer, pattern) ->
|
||||||
|
found = []
|
||||||
|
buffer.scan(new RegExp(pattern, 'g'), (obj) -> found.push obj.range)
|
||||||
|
return found
|
||||||
|
|
||||||
|
findNextInBuffer : (buffer, curPos, pattern) ->
|
||||||
|
found = @findInBuffer(buffer, pattern)
|
||||||
|
more = (i for i in found when i.compare([curPos, curPos]) is 1)
|
||||||
|
if more.length > 0
|
||||||
|
return more[0].start.row
|
||||||
|
else if found.length > 0
|
||||||
|
return found[0].start.row
|
||||||
|
else
|
||||||
|
return null
|
||||||
|
|
||||||
|
findPreviousInBuffer : (buffer, curPos, pattern) ->
|
||||||
|
found = @findInBuffer(buffer, pattern)
|
||||||
|
console.log found, curPos
|
||||||
|
less = (i for i in found when i.compare([curPos, curPos]) is -1)
|
||||||
|
if less.length > 0
|
||||||
|
return less[less.length - 1].start.row
|
||||||
|
else if found.length > 0
|
||||||
|
return found[found.length - 1].start.row
|
||||||
|
else
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
class GlobalExState
|
class GlobalExState
|
||||||
commandHistory: []
|
commandHistory: []
|
||||||
|
setVim: (@vim) ->
|
||||||
|
|
||||||
module.exports = GlobalExState
|
module.exports = GlobalExState
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue