From 32af5b18f504d011e38ff29b1ddff0c7d5c86798 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sat, 21 Mar 2015 18:54:44 +0100 Subject: [PATCH 01/15] Fix consumeVim version so we can use it --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7a40a3..152e65e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "consumedServices": { "vim-mode": { "versions": { - "^0.33.0": "consumeVim" + "^0.1.0": "consumeVim" } } }, From 1ec458983103433655644aea798b54cc3aec0683 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sat, 21 Mar 2015 19:02:00 +0100 Subject: [PATCH 02/15] Provide the vim-mode service globally --- lib/ex-mode.coffee | 2 +- lib/global-ex-state.coffee | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ex-mode.coffee b/lib/ex-mode.coffee index 6ce36d8..1a4979d 100644 --- a/lib/ex-mode.coffee +++ b/lib/ex-mode.coffee @@ -32,5 +32,5 @@ module.exports = ExMode = registerCommand: Ex.registerCommand.bind(Ex) consumeVim: (vim) -> - console.log vim @vim = vim + @globalExState.setVim(vim) diff --git a/lib/global-ex-state.coffee b/lib/global-ex-state.coffee index 5325113..be51b85 100644 --- a/lib/global-ex-state.coffee +++ b/lib/global-ex-state.coffee @@ -1,4 +1,5 @@ class GlobalExState commandHistory: [] + setVim: (@vim) -> module.exports = GlobalExState From a7504aa5905ccf7d883b0ba3e6242deb0b064353 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sat, 21 Mar 2015 19:03:58 +0100 Subject: [PATCH 03/15] Address parsing --- lib/command.coffee | 120 ++++++++++++++++++++++++++++++++++++++++++++- lib/find.coffee | 26 ++++++++++ 2 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 lib/find.coffee 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] +} From 548f9b75c47c6496e1a8688254903544f7464b00 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sat, 21 Mar 2015 23:36:08 +0100 Subject: [PATCH 04/15] Add actual command calling --- lib/command.coffee | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/lib/command.coffee b/lib/command.coffee index f7487ca..eecf187 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -27,11 +27,11 @@ class Command unless mark? throw new CommandError('Mark ' + str + ' not set.') addr = mark.bufferMarker.range.end.row - else if str[0] == "/" # TODO: Parse forward search + else if str[0] == "/" 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 + else if str[0] == "?" addr = Find.findPrevious(@editor.buffer.lines, str[1...-1], curLine) unless addr? throw new CommandError('Pattern not found: ' + str[1...-1]) @@ -53,7 +53,8 @@ class Command 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 + # 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)*/, '') @@ -124,15 +125,43 @@ class Command # Step 5: Leading blanks are ignored cl = cl.trimLeft() - # Vim behavior: If no command is specified, go to the specified length + # Step 6a: If no command is specified, go to the last specified address if cl.length == 0 @editor.setCursorBufferPosition([range[1], 0]) return - func = Ex.singleton()[command] - if func? - func(args...) + # 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 is equal to :mark - only a-zA-Z is + # in vim-mode for now + if cl.length == 2 and cl[0] == '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 - throw new CommandError("#{input.characters}") + [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) == 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, CommandError} From 5fd7a8b6dd24063d437854e8ea017f927c627c81 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 22 Mar 2015 00:53:26 +0100 Subject: [PATCH 05/15] Fix line length --- lib/ex.coffee | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 024f04c..15ae358 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -9,15 +9,21 @@ trySave = (func) -> catch error if error.message.endsWith('is a directory') atom.notifications.addWarning("Unable to save file: #{error.message}") - else if error.code is 'EACCES' and error.path? - atom.notifications.addWarning("Unable to save file: Permission denied '#{error.path}'") - else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST'] and error.path? - atom.notifications.addWarning("Unable to save file '#{error.path}'", detail: error.message) - else if error.code is 'EROFS' and error.path? - atom.notifications.addWarning("Unable to save file: Read-only file system '#{error.path}'") - else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message) + else if error.path? + if error.code is 'EACCES' + atom.notifications + .addWarning("Unable to save file: Permission denied '#{error.path}'") + else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST'] + atom.notifications.addWarning("Unable to save file '#{error.path}'", + 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] - 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 throw error @@ -110,7 +116,7 @@ class Ex wq: (filePath) => @write(filePath).then => @quit() - + x: => @wq() wa: -> From e5e944656c59ba7241d3090a2a04d9d16dac4922 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 22 Mar 2015 00:58:53 +0100 Subject: [PATCH 06/15] is instead of == --- lib/command.coffee | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/command.coffee b/lib/command.coffee index eecf187..136863d 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -11,27 +11,27 @@ class Command @viewModel = new ExViewModel(@) parseAddr: (str, curLine) -> - if str == '.' + if str is '.' addr = curLine - else if str == '$' + 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 = curLine + @parseOffset(str) else if not isNaN(str) addr = parseInt(str) - 1 - else if str[0] == "'" # Parse Mark... + 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] == "/" + else if str[0] is "/" 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] == "?" + else if str[0] is "?" addr = Find.findPrevious(@editor.buffer.lines, str[1...-1], curLine) unless addr? throw new CommandError('Pattern not found: ' + str[1...-1]) @@ -39,13 +39,13 @@ class Command return addr parseOffset: (str) -> - if str.length == 0 + if str.length is 0 return 0 - if str.length == 1 + if str.length is 1 o = 1 else o = parseInt(str[1..]) - if str[0] == '+' + if str[0] is '+' return o else return -o @@ -60,11 +60,11 @@ class Command 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] == '"' + if cl[0] is '"' return # Step 4: Address parsing lastLine = @editor.getBuffer().lines.length - 1 - if cl[0] == '%' + if cl[0] is '%' range = [0, lastLine] cl = cl[1..] else @@ -126,7 +126,7 @@ class Command cl = cl.trimLeft() # Step 6a: If no command is specified, go to the last specified address - if cl.length == 0 + if cl.length is 0 @editor.setCursorBufferPosition([range[1], 0]) return @@ -137,7 +137,7 @@ class Command # Step 7b: :k is equal to :mark - only a-zA-Z is # in vim-mode for now - if cl.length == 2 and cl[0] == 'k' and /[a-z]/i.test(cl[1]) + 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]) @@ -152,7 +152,7 @@ class Command else # Step 8: Match command against existing commands matching = ([name for name, val of Ex.singleton() when \ - name.indexOf(command) == 0]) + name.indexOf(command) is 0]) matching.sort() From 054473a3d187369c9ac31a57396ce9e1b7b355ff Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 22 Mar 2015 01:11:01 +0100 Subject: [PATCH 07/15] Fix --- lib/command.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/command.coffee b/lib/command.coffee index 136863d..e22566b 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -147,7 +147,7 @@ class Command [m, command, args] = cl.match(/^(\w+)(.*)/) # If the command matches an existing one exactly, execute that one - if func = Ex.singleton()[command]? + if (func = Ex.singleton()[command])? func(range, args) else # Step 8: Match command against existing commands From 8b658db0902d4a9b25694b33e73a2413c81cdca8 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 22 Mar 2015 01:11:34 +0100 Subject: [PATCH 08/15] Update commands --- lib/ex.coffee | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 15ae358..bea6234 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -41,7 +41,9 @@ class Ex q: => @quit() - tabedit: (filePaths...) -> + tabedit: (range, args) -> + args = args.trimLeft() + filePaths = args.split(' ') pane = atom.workspace.getActivePane() if filePaths? and filePaths.length > 0 for file in filePaths @@ -49,9 +51,9 @@ class Ex else atom.workspace.openURIInPane('', pane) - tabe: (filePaths...) => @tabedit(filePaths...) + tabe: (args...) => @tabedit(args...) - tabnew: (filePaths...) => @tabedit(filePaths...) + tabnew: (args...) => @tabedit(args...) tabclose: => @quit() @@ -69,13 +71,14 @@ class Ex tabp: => @tabprevious() - edit: (filePath) => @tabedit(filePath) if filePath? + edit: (range, filePath) => @tabedit(range, filePath) if filePath? - e: (filePath) => @edit(filePath) + e: (args...) => @edit(args...) enew: => @edit() - write: (filePath) -> + write: (range, filePath) -> + filePath = filePath.trimLeft() deferred = Promise.defer() projectPath = atom.project.getPath() @@ -111,18 +114,20 @@ class Ex deferred.promise - w: (filePath) => - @write(filePath) + w: (args...) => + @write(args...) - wq: (filePath) => - @write(filePath).then => @quit() + wq: (args...) => + @write(args...).then => @quit() x: => @wq() wa: -> atom.workspace.saveAll() - split: (filePaths...) -> + split: (range, args) -> + args = args.trimLeft() + filePaths = args.splitLeft() pane = atom.workspace.getActivePane() if filePaths? and filePaths.length > 0 newPane = pane.splitUp() @@ -132,9 +137,11 @@ class Ex else pane.splitUp(copyActiveItem: true) - sp: (filePaths...) => @split(filePaths...) + sp: (args...) => @split(args...) - vsplit: (filePaths...) -> + vsplit: (range, args) -> + args = args.trimLeft() + filePaths = args.split(' ') pane = atom.workspace.getActivePane() if filePaths? and filePaths.length > 0 newPane = pane.splitLeft() @@ -144,6 +151,6 @@ class Ex else pane.splitLeft(copyActiveItem: true) - vsp: (filePaths...) => @vsplit(filePaths...) + vsp: (args...) => @vsplit(args...) module.exports = Ex From d1295587ee1ea054d5d73ea3d0f54296db185a12 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 22 Mar 2015 02:56:48 +0100 Subject: [PATCH 09/15] Update commands --- lib/ex.coffee | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index bea6234..6a14cd6 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -29,6 +29,10 @@ trySave = (func) -> deferred.promise +getFullPath = (filePath) -> + return filePath if path.isAbsolute(filePath) + return path.join(atom.project.getPath(), filePath) + class Ex @singleton: => @ex ||= new Ex @@ -42,7 +46,7 @@ class Ex q: => @quit() tabedit: (range, args) -> - args = args.trimLeft() + args = args.trim() filePaths = args.split(' ') pane = atom.workspace.getActivePane() if filePaths? and filePaths.length > 0 @@ -71,26 +75,32 @@ class Ex tabp: => @tabprevious() - edit: (range, filePath) => @tabedit(range, 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: (args...) => @edit(args...) - enew: => @edit() + enew: -> + buffer = atom.workspace.getActiveEditor().buffer + buffer.setPath(undefined) + buffer.load() write: (range, filePath) -> - filePath = filePath.trimLeft() + filePath = filePath.trim() deferred = Promise.defer() - projectPath = atom.project.getPath() pane = atom.workspace.getActivePane() editor = atom.workspace.getActiveEditor() if atom.workspace.getActiveTextEditor().getPath() isnt undefined - if filePath? + if filePath.length > 0 editorPath = editor.getPath() - fullPath = if path.isAbsolute(filePath) - filePath - else - path.join(projectPath, filePath) + fullPath = getFullPath(filePath) trySave(-> editor.saveAs(fullPath)) .then -> deferred.resolve() @@ -99,11 +109,8 @@ class Ex trySave(-> editor.save()) .then deferred.resolve else - if filePath? - fullPath = if path.isAbsolute(filePath) - filePath - else - path.join(projectPath, filePath) + if filePath.length > 0 + fullPath = getFullPath(filePath) trySave(-> editor.saveAs(fullPath)) .then deferred.resolve else @@ -126,8 +133,10 @@ class Ex atom.workspace.saveAll() split: (range, args) -> - args = args.trimLeft() - filePaths = args.splitLeft() + 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() if filePaths? and filePaths.length > 0 newPane = pane.splitUp() @@ -140,8 +149,9 @@ class Ex sp: (args...) => @split(args...) vsplit: (range, args) -> - args = args.trimLeft() + args = args.trim() filePaths = args.split(' ') + filePaths = undefined if filePaths.length is 1 and filePaths[0] is '' pane = atom.workspace.getActivePane() if filePaths? and filePaths.length > 0 newPane = pane.splitLeft() From 433b1dd6ac44450d9ab34127b6ff12651c88d02d Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 22 Mar 2015 14:17:43 +0100 Subject: [PATCH 10/15] Yes we can --- lib/command.coffee | 18 +++++++++--------- lib/find.coffee | 37 +++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/command.coffee b/lib/command.coffee index e22566b..a346b8c 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -10,14 +10,14 @@ class Command constructor: (@editor, @exState) -> @viewModel = new ExViewModel(@) - parseAddr: (str, curLine) -> + parseAddr: (str, curPos) -> if str is '.' - addr = curLine + 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 = curLine + @parseOffset(str) + addr = curPos.row + @parseOffset(str) else if not isNaN(str) addr = parseInt(str) - 1 else if str[0] is "'" # Parse Mark... @@ -28,11 +28,11 @@ class Command throw new CommandError('Mark ' + str + ' not set.') addr = mark.bufferMarker.range.end.row else if str[0] is "/" - addr = Find.findNext(@editor.buffer.lines, str[1...-1], curLine) + 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.findPrevious(@editor.buffer.lines, str[1...-1], curLine) + addr = Find.findPreviousInBuffer(@editor.buffer, curPos, str[1...-1]) unless addr? throw new CommandError('Pattern not found: ' + str[1...-1]) @@ -95,13 +95,13 @@ class Command [match, addr1, off1, addr2, off2] = cl.match(addrPattern) - curLine = @editor.getCursorBufferPosition().row + curPos = @editor.getCursorBufferPosition() if addr1? - address1 = @parseAddr(addr1, curLine) + address1 = @parseAddr(addr1, curPos) else # If no addr1 is given (,+3), assume it is '.' - address1 = curLine + address1 = curPos.row if off1? address1 += @parseOffset(off1) @@ -109,7 +109,7 @@ class Command throw new CommandError('Invalid range') if addr2? - address2 = @parseAddr(addr2, curLine) + address2 = @parseAddr(addr2, curPos) if off2? address2 += @parseOffset(off2) diff --git a/lib/find.coffee b/lib/find.coffee index 046ef4f..2f0c597 100644 --- a/lib/find.coffee +++ b/lib/find.coffee @@ -1,26 +1,27 @@ 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)) + findInBuffer : (buffer, pattern) -> + found = [] + buffer.scan(new RegExp(pattern, 'g'), (obj) -> found.push obj.range) + return found - findNext: (lines, pattern, curLine) -> - lines = @findLines(lines, pattern) - if lines.length == 0 - return null - more = (i for i in lines when i > curLine) + 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] + return more[0].start.row + else if found.length > 0 + return found[0].start.row 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) + + 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[0] + return less[less.length - 1].start.row + else if found.length > 0 + return found[found.length - 1].start.row else - return lines[lines.length - 1] + return null } From 477b2a6b6bd1a7a2d4cd265105c18a1aa5d728fb Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 22 Mar 2015 14:34:37 +0100 Subject: [PATCH 11/15] Fix double comprehension --- lib/command.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/command.coffee b/lib/command.coffee index a346b8c..d11ad7d 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -151,8 +151,8 @@ class 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 = (name for name, val of Ex.singleton() when \ + name.indexOf(command) is 0) matching.sort() From 161c55c94fc2cfab299fa77d8c4b341ebee9f5dd Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 22 Mar 2015 15:19:41 +0100 Subject: [PATCH 12/15] Style --- lib/command.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/command.coffee b/lib/command.coffee index d11ad7d..5fb60de 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -25,16 +25,16 @@ class Command throw new CommandError("Couldn't get access to vim-mode.") mark = @vimState.marks[str[1]] unless mark? - throw new CommandError('Mark ' + str + ' not set.') + 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]) + 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]) + throw new CommandError("Pattern not found: #{str[1...-1]}") return addr From f662a96afd97029ea3b4095fd17d6583489fc7a3 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 22 Mar 2015 23:14:56 +0100 Subject: [PATCH 13/15] Implement :s --- lib/command-error.coffee | 5 +++ lib/command.coffee | 7 ++-- lib/ex-state.coffee | 3 +- lib/ex.coffee | 69 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 lib/command-error.coffee diff --git a/lib/command-error.coffee b/lib/command-error.coffee new file mode 100644 index 0000000..c9861e9 --- /dev/null +++ b/lib/command-error.coffee @@ -0,0 +1,5 @@ +class CommandError + constructor: (@message) -> + @name = 'Command Error' + +module.exports = CommandError diff --git a/lib/command.coffee b/lib/command.coffee index 5fb60de..0b5849b 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -1,10 +1,7 @@ ExViewModel = require './ex-view-model' Ex = require './ex' Find = require './find' - -class CommandError - constructor: (@message) -> - @name = 'Command Error' +CommandError = require './command-error' class Command constructor: (@editor, @exState) -> @@ -164,4 +161,4 @@ class Command else throw new CommandError("Not an editor command: #{input.characters}") -module.exports = {Command, CommandError} +module.exports = Command diff --git a/lib/ex-state.coffee b/lib/ex-state.coffee index 7aff344..e52ea1c 100644 --- a/lib/ex-state.coffee +++ b/lib/ex-state.coffee @@ -1,6 +1,7 @@ {Emitter, Disposable, CompositeDisposable} = require 'event-kit' -{Command, CommandError} = require './command' +Command = require './command' +CommandError = require './command-error' class ExState constructor: (@editorElement, @globalExState) -> diff --git a/lib/ex.coffee b/lib/ex.coffee index 6a14cd6..0981945 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -1,4 +1,5 @@ path = require 'path' +CommandError = require './command-error' trySave = (func) -> deferred = Promise.defer() @@ -33,6 +34,23 @@ 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 @singleton: => @ex ||= new Ex @@ -148,6 +166,57 @@ class Ex sp: (args...) => @split(args...) + 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 + + console.log pattern, [[range[0], 0]] + buffer = atom.workspace.getActiveTextEditor().buffer + # This adds an entry to the history for each replacement + for line in [range[0]..range[1]] + console.log line + buffer.scanInRange(pattern, + [[line, 0], [line, buffer.lines[line].length]], + ({match, matchText, range, stop, replace}) -> + replace(replaceGroups(match[..], spl[1])) + ) + # This finds all matches in the buffer, sadly + # buffer.scanInRange(pattern, + # [[range[0], 0], [range[1], buffer.lines[range[1]]].length], + # ({match, matchText, range, stop, replace}) -> + # console.log match, matchText + # ) + + s: (args...) => @substitute(args...) + vsplit: (range, args) -> args = args.trim() filePaths = args.split(' ') From 822d5fcf9797c3dca8e7be3b308731c792fc9318 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 22 Mar 2015 23:30:01 +0100 Subject: [PATCH 14/15] Stop debug logging --- lib/ex.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 0981945..4467dbc 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -198,11 +198,9 @@ class Ex else throw e - console.log pattern, [[range[0], 0]] buffer = atom.workspace.getActiveTextEditor().buffer # This adds an entry to the history for each replacement for line in [range[0]..range[1]] - console.log line buffer.scanInRange(pattern, [[line, 0], [line, buffer.lines[line].length]], ({match, matchText, range, stop, replace}) -> From 241abdb9d07b22c70219f079981bbfb842ae25ac Mon Sep 17 00:00:00 2001 From: jazzpi Date: Thu, 26 Mar 2015 15:35:14 +0100 Subject: [PATCH 15/15] Fix :s behaviour --- lib/ex.coffee | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 4467dbc..874c9f7 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -199,19 +199,14 @@ class Ex throw e buffer = atom.workspace.getActiveTextEditor().buffer - # This adds an entry to the history for each replacement + 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])) ) - # This finds all matches in the buffer, sadly - # buffer.scanInRange(pattern, - # [[range[0], 0], [range[1], buffer.lines[range[1]]].length], - # ({match, matchText, range, stop, replace}) -> - # console.log match, matchText - # ) + buffer.history.groupChangesSinceCheckpoint(cp) s: (args...) => @substitute(args...)