Merge pull request #26 from jazzpi/patch-3

Implement parsing following ex spec and :s, fix behaviour of :e
This commit is contained in:
Loic Nageleisen 2015-03-30 10:11:17 +02:00
commit 4da6f2d987
7 changed files with 310 additions and 48 deletions

5
lib/command-error.coffee Normal file
View file

@ -0,0 +1,5 @@
class CommandError
constructor: (@message) ->
@name = 'Command Error'
module.exports = CommandError

View file

@ -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

View file

@ -33,3 +33,4 @@ module.exports = ExMode =
consumeVim: (vim) -> consumeVim: (vim) ->
@vim = vim @vim = vim
@globalExState.setVim(vim)

View file

@ -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) ->

View file

@ -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
View 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
}

View file

@ -1,4 +1,5 @@
class GlobalExState class GlobalExState
commandHistory: [] commandHistory: []
setVim: (@vim) ->
module.exports = GlobalExState module.exports = GlobalExState