diff --git a/lib/command.coffee b/lib/command.coffee index 851f859..f7487ca 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -1,5 +1,6 @@ ExViewModel = require './ex-view-model' Ex = require './ex' +Find = require './find' class CommandError constructor: (@message) -> @@ -9,9 +10,124 @@ class Command constructor: (@editor, @exState) -> @viewModel = new ExViewModel(@) + parseAddr: (str, curLine) -> + if str == '.' + addr = curLine + else if str == '$' + # Lines are 0-indexed in Atom, but 1-indexed in vim. + addr = @editor.getBuffer().lines.length - 1 + else if str[0] in ["+", "-"] + addr = curLine + @parseOffset(str) + else if not isNaN(str) + addr = parseInt(str) - 1 + else if str[0] == "'" # 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] == "/" # TODO: Parse forward search + addr = Find.findNext(@editor.buffer.lines, str[1...-1], curLine) + unless addr? + throw new CommandError('Pattern not found: ' + str[1...-1]) + else if str[0] == "?" # TODO: Parse backwards search + addr = Find.findPrevious(@editor.buffer.lines, str[1...-1], curLine) + unless addr? + throw new CommandError('Pattern not found: ' + str[1...-1]) + + return addr + + parseOffset: (str) -> + if str.length == 0 + return 0 + if str.length == 1 + o = 1 + else + o = parseInt(str[1..]) + if str[0] == '+' + return o + else + return -o + execute: (input) -> - return unless input.characters.length > 0 - [command, args...] = input.characters.split(" ") + @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] == '"' + return + # Step 4: Address parsing + lastLine = @editor.getBuffer().lines.length - 1 + if cl[0] == '%' + 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) + + curLine = @editor.getCursorBufferPosition().row + + if addr1? + address1 = @parseAddr(addr1, curLine) + else + # If no addr1 is given (,+3), assume it is '.' + address1 = curLine + if off1? + address1 += @parseOffset(off1) + + if address1 < 0 or address1 > lastLine + throw new CommandError('Invalid range') + + if addr2? + address2 = @parseAddr(addr2, curLine) + 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() + + # Vim behavior: If no command is specified, go to the specified length + if cl.length == 0 + @editor.setCursorBufferPosition([range[1], 0]) + return func = Ex.singleton()[command] if func? diff --git a/lib/find.coffee b/lib/find.coffee new file mode 100644 index 0000000..046ef4f --- /dev/null +++ b/lib/find.coffee @@ -0,0 +1,26 @@ +module.exports = { + findLines: (lines, pattern) -> + # TODO: There's gotta be a better way to do this. Can we use vim-mode's + # search or find-and-replace maybe? + return (i for line, i in lines when line.match(pattern)) + + findNext: (lines, pattern, curLine) -> + lines = @findLines(lines, pattern) + if lines.length == 0 + return null + more = (i for i in lines when i > curLine) + if more.length > 0 + return more[0] + else + return lines[0] + + findPrevious: (lines, pattern, curLine) -> + lines = @findLines(lines, pattern) + if lines.length == 0 + return null + less = (i for i in lines when i < curLine) + if less.length > 0 + return less[0] + else + return lines[lines.length - 1] +}