From cb2d79a1e3f6a50d8c527dd36fb045e6a5711bf1 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Sun, 24 May 2015 15:55:24 +0200 Subject: [PATCH 001/131] get some fresh air --- lib/command.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/command.coffee b/lib/command.coffee index 1839086..115b273 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -52,13 +52,16 @@ class Command # 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 '%' From 3c17b6e670554230c4f54f5f2f55c45df08a8d0d Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Sun, 24 May 2015 15:56:11 +0200 Subject: [PATCH 002/131] history even bad commands (easing fixups) --- lib/ex-state.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ex-state.coffee b/lib/ex-state.coffee index e52ea1c..b676535 100644 --- a/lib/ex-state.coffee +++ b/lib/ex-state.coffee @@ -45,9 +45,9 @@ class ExState processOpStack: -> [command, input] = @opStack if input.characters.length > 0 + @history.unshift command try command.execute(input) - @history.unshift command catch e if (e instanceof CommandError) atom.notifications.addError("Command error: #{e.message}") From c43b2658efcfa6786e573b743d308476ca844496 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Sun, 24 May 2015 15:56:53 +0200 Subject: [PATCH 003/131] stop spurious cancels --- lib/view-model.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/view-model.coffee b/lib/view-model.coffee index 5e761b1..af96e77 100644 --- a/lib/view-model.coffee +++ b/lib/view-model.coffee @@ -7,12 +7,16 @@ class ViewModel @view = new ExCommandModeInputElement().initialize(@, opts) @editor.commandModeInputView = @view @exState.onDidFailToExecute => @view.remove() + @done = false confirm: (view) -> @exState.pushOperations(new Input(@view.value)) + @done = true cancel: (view) -> - @exState.pushOperations(new Input('')) + unless @done + @exState.pushOperations(new Input('')) + @done = true class Input constructor: (@characters) -> From e33dc15392ca5eeecaf72b3cefad031d08c3d258 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Sun, 24 May 2015 15:57:13 +0200 Subject: [PATCH 004/131] Prepare 0.5.1 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6988a99..3364f3e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.5.0", + "version": "0.5.1", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 5ee749cb0b6b55e07efeeffa5e1e865baa218be7 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 27 May 2015 17:29:37 +0200 Subject: [PATCH 005/131] Fix :write behaviour --- lib/ex.coffee | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index c159534..21f5037 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -1,5 +1,6 @@ path = require 'path' CommandError = require './command-error' +fs = require 'fs' trySave = (func) -> deferred = Promise.defer() @@ -30,6 +31,10 @@ trySave = (func) -> deferred.promise +saveAs = (filePath) -> + editor = atom.workspace.getActiveTextEditor() + fs.writeFileSync(filePath, editor.getText()) + getFullPath = (filePath) -> return filePath if path.isAbsolute(filePath) return path.join(atom.project.getPath(), filePath) @@ -119,7 +124,7 @@ class Ex if filePath.length > 0 editorPath = editor.getPath() fullPath = getFullPath(filePath) - trySave(-> editor.saveAs(fullPath)) + trySave(-> saveAs(fullPath)) .then -> deferred.resolve() editor.buffer.setPath(editorPath) @@ -129,7 +134,7 @@ class Ex else if filePath.length > 0 fullPath = getFullPath(filePath) - trySave(-> editor.saveAs(fullPath)) + trySave(-> saveAs(fullPath)) .then deferred.resolve else fullPath = atom.showSaveDialogSync() From 4cccef79a5c15ad0f65bc5e625234f63f300ac03 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 10 Jun 2015 18:19:59 +0200 Subject: [PATCH 006/131] Replace `\#{delimiter}` with `#{delimiter}` in :s --- lib/ex.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/ex.coffee b/lib/ex.coffee index 21f5037..677b4a6 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -190,6 +190,9 @@ class Ex throw new CommandError('Trailing characters') spl[1] ?= '' spl[2] ?= '' + notDelimRE = new RegExp("\\\\#{delim}", 'g') + spl[0] = spl[0].replace(notDelimRE, delim) + spl[1] = spl[1].replace(notDelimRE, delim) try pattern = new RegExp(spl[0], spl[2]) From 7c202faefa90e6a1acedc658597a440f3d6ce3e9 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Thu, 18 Jun 2015 18:49:37 +0200 Subject: [PATCH 007/131] Use TextEditor.transact This fixes #57 and atom/atom#703 --- lib/ex.coffee | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 21f5037..3231748 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -203,14 +203,13 @@ class Ex 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) + atom.workspace.getActiveTextEditor().transact -> + 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])) + ) s: (args...) => @substitute(args...) From 928e5626fd4816acb12d3b2d4b8e7740062ec91e Mon Sep 17 00:00:00 2001 From: Dagan McGregor Date: Sun, 21 Jun 2015 03:29:18 +1200 Subject: [PATCH 008/131] Update ex-mode.less Display ':' at start of the command input --- styles/ex-mode.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/styles/ex-mode.less b/styles/ex-mode.less index da5d1df..288624e 100644 --- a/styles/ex-mode.less +++ b/styles/ex-mode.less @@ -6,3 +6,7 @@ .ex-mode { } + +.command-mode-input atom-text-editor[mini]::before { + content: ":"; +} From 44f296999b38160f375a7ce317b8fecd15cbefde Mon Sep 17 00:00:00 2001 From: Dagan McGregor Date: Tue, 23 Jun 2015 18:12:02 +1200 Subject: [PATCH 009/131] Update ex-mode.less Change to div to avoid conflict with vim-mode search --- styles/ex-mode.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/ex-mode.less b/styles/ex-mode.less index 288624e..fea8230 100644 --- a/styles/ex-mode.less +++ b/styles/ex-mode.less @@ -7,6 +7,6 @@ .ex-mode { } -.command-mode-input atom-text-editor[mini]::before { +div[is=ex-command-mode-input] atom-text-editor[mini]::before { content: ":"; } From 0f38fd195ab6ad97222e7e4954365a72f5a3b5d9 Mon Sep 17 00:00:00 2001 From: Nikita Zyuzin Date: Mon, 13 Jul 2015 23:06:03 +0400 Subject: [PATCH 010/131] Add support for :set [option] --- lib/ex.coffee | 24 ++++++++++++++++++++++++ lib/vim-option.coffee | 23 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 lib/vim-option.coffee diff --git a/lib/ex.coffee b/lib/ex.coffee index 3231748..334781d 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -1,6 +1,7 @@ path = require 'path' CommandError = require './command-error' fs = require 'fs' +VimOption = require './vim-option' trySave = (func) -> deferred = Promise.defer() @@ -232,4 +233,27 @@ class Ex range = [[range[0], 0], [range[1] + 1, 0]] atom.workspace.getActiveTextEditor().buffer.setTextInRange(range, '') + set: (range, args) -> + args = args.trim() + if args == "" + throw new CommandError("No option specified") + options = args.split(' ') + for option in options + do -> + if option.includes("=") + nameValPair = option.split("=") + if (nameValPair.length != 2) + throw new CommandError("Wrong option format. [name]=[value] format is expected") + optionName = nameValPair[0] + optionValue = nameValPair[1] + optionProcessor = VimOption.singleton()[optionName] + if not optionProcessor? + throw new CommandError("No such option: #{optionName}") + optionProcessor(optionValue) + else + optionProcessor = VimOption.singleton()[option] + if not optionProcessor? + throw new CommandError("No such option: #{option}") + optionProcessor() + module.exports = Ex diff --git a/lib/vim-option.coffee b/lib/vim-option.coffee new file mode 100644 index 0000000..2ee056c --- /dev/null +++ b/lib/vim-option.coffee @@ -0,0 +1,23 @@ +class VimOption + @singleton: => + @option ||= new VimOption + + list: => + atom.config.set("editor.showInvisibles", true) + + nolist: => + atom.config.set("editor.showInvisibles", false) + + number: => + atom.config.set("editor.showLineNumbers", true) + + nu: => + @number() + + nonumber: => + atom.config.set("editor.showLineNumbers", false) + + nonu: => + @nonumber() + +module.exports = VimOption From 656ed90f7e5391ce7228f9da00f77423573403c2 Mon Sep 17 00:00:00 2001 From: Matthew Leeds Date: Mon, 27 Jul 2015 15:23:55 -0500 Subject: [PATCH 011/131] Simplify how :write works and make it work when no projects are open. --- lib/ex.coffee | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 3231748..78168ac 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -36,8 +36,16 @@ saveAs = (filePath) -> fs.writeFileSync(filePath, editor.getText()) getFullPath = (filePath) -> - return filePath if path.isAbsolute(filePath) - return path.join(atom.project.getPath(), filePath) + if filePath is '' + throw new Error + if path.isAbsolute(filePath) + return filePath + else if atom.workspace.getActiveTextEditor().getPath()? + return path.join(path.dirname(atom.workspace.getActiveTextEditor().getPath()), filePath) + else if atom.project.getPaths()[0]? + return path.join(atom.project.getPaths()[0], filePath) + else + throw new Error replaceGroups = (groups, replString) -> arr = replString.split('') @@ -99,7 +107,7 @@ class Ex tabp: => @tabprevious() edit: (range, filePath) -> - filePath = filePath.trim() + filePath = path.normalize(filePath.trim()) if filePath.indexOf(' ') isnt -1 throw new CommandError('Only one file name allowed') buffer = atom.workspace.getActiveTextEditor().buffer @@ -115,32 +123,18 @@ class Ex buffer.load() write: (range, filePath) -> - filePath = filePath.trim() + filePath = path.normalize(filePath.trim()) deferred = Promise.defer() - pane = atom.workspace.getActivePane() editor = atom.workspace.getActiveTextEditor() - if atom.workspace.getActiveTextEditor().getPath() isnt undefined - if filePath.length > 0 - editorPath = editor.getPath() - fullPath = getFullPath(filePath) - trySave(-> saveAs(fullPath)) - .then -> - deferred.resolve() - editor.buffer.setPath(editorPath) - else - trySave(-> editor.save()) - .then deferred.resolve - else - if filePath.length > 0 - fullPath = getFullPath(filePath) - trySave(-> saveAs(fullPath)) - .then deferred.resolve - else - fullPath = atom.showSaveDialogSync() - if fullPath? - trySave(-> editor.saveAs(fullPath)) - .then deferred.resolve + try + fullPath = getFullPath(filePath) + catch error + fullPath = atom.showSaveDialogSync() + if fullPath? + trySave(-> editor.saveAs(fullPath)) + .then deferred.resolve + editor.buffer.setPath(fullPath) deferred.promise From ea919ada292973751177343ec3788350c74b4139 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 28 Jul 2015 11:56:45 +0200 Subject: [PATCH 012/131] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad57372..9aa8d03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.6.0 +* No project/multiple projects paths (uses first one) +* Support for :set +* Fixes + ## 0.5.0 * Comply with upcoming Atom API 1.0 * Added `:d` From cdf65d6e27df1475262db07688a542b7aaeb7542 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 28 Jul 2015 11:57:07 +0200 Subject: [PATCH 013/131] Prepare 0.6.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3364f3e..1f0124a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.5.1", + "version": "0.6.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 8fd1fe14c8a7d366f1a9be699a34339858a78480 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 28 Jul 2015 12:02:03 +0200 Subject: [PATCH 014/131] Added ~ support Thanks to @romgrk in PR #48 --- lib/ex.coffee | 6 +++--- package.json | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index b3e2d4d..c7f0d45 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -1,6 +1,6 @@ path = require 'path' CommandError = require './command-error' -fs = require 'fs' +fs = require 'fs-plus' VimOption = require './vim-option' trySave = (func) -> @@ -108,7 +108,7 @@ class Ex tabp: => @tabprevious() edit: (range, filePath) -> - filePath = path.normalize(filePath.trim()) + filePath = fs.normalize(filePath.trim()) if filePath.indexOf(' ') isnt -1 throw new CommandError('Only one file name allowed') buffer = atom.workspace.getActiveTextEditor().buffer @@ -124,7 +124,7 @@ class Ex buffer.load() write: (range, filePath) -> - filePath = path.normalize(filePath.trim()) + filePath = fs.normalize(filePath.trim()) deferred = Promise.defer() editor = atom.workspace.getActiveTextEditor() diff --git a/package.json b/package.json index 1f0124a..bf41efc 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "underscore-plus": "1.x", "event-kit": "^0.7.2", "space-pen": "^5.1.1", - "atom-space-pen-views": "^2.0.4" + "atom-space-pen-views": "^2.0.4", + "fs-plus": "^2.2.8" }, "consumedServices": { "vim-mode": { From da8405b38737a29d6df04e70b6f0e2c937582546 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 28 Jul 2015 12:03:04 +0200 Subject: [PATCH 015/131] Prepare 0.6.1 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bf41efc..9b4cef3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.6.0", + "version": "0.6.1", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 84c548a444bfc62dc2f7e45e72b71cf0d5835c3e Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 29 Jul 2015 16:31:42 +0200 Subject: [PATCH 016/131] don't :e an unsaved file --- lib/ex.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index c7f0d45..6476bad 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -112,7 +112,10 @@ class Ex if filePath.indexOf(' ') isnt -1 throw new CommandError('Only one file name allowed') buffer = atom.workspace.getActiveTextEditor().buffer - filePath = buffer.getPath() if filePath is '' + if buffer.isModified() + throw new CommandError('Unsaved file') + if filePath is '' + filePath = buffer.getPath() buffer.setPath(getFullPath(filePath)) buffer.load() From 1a117bddf903ee934cb3388c61414039a5d618f5 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 29 Jul 2015 16:36:55 +0200 Subject: [PATCH 017/131] Clean up save logic (fixes #75) --- lib/ex.coffee | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 6476bad..7b7c349 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -37,16 +37,14 @@ saveAs = (filePath) -> fs.writeFileSync(filePath, editor.getText()) getFullPath = (filePath) -> - if filePath is '' - throw new Error if path.isAbsolute(filePath) - return filePath - else if atom.workspace.getActiveTextEditor().getPath()? - return path.join(path.dirname(atom.workspace.getActiveTextEditor().getPath()), filePath) - else if atom.project.getPaths()[0]? - return path.join(atom.project.getPaths()[0], filePath) + fullPath = filePath + else if atom.project.getPaths().length == 0 + fullPath = path.join('~', filePath) else - throw new Error + fullPath = path.join(atom.project.getPaths()[0], filePath) + + return fs.normalize(fullPath) replaceGroups = (groups, replString) -> arr = replString.split('') @@ -127,18 +125,33 @@ class Ex buffer.load() write: (range, filePath) -> - filePath = fs.normalize(filePath.trim()) + filePath = filePath.trim() deferred = Promise.defer() + pane = atom.workspace.getActivePane() editor = atom.workspace.getActiveTextEditor() - try - fullPath = getFullPath(filePath) - catch error - fullPath = atom.showSaveDialogSync() - if fullPath? - trySave(-> editor.saveAs(fullPath)) - .then deferred.resolve - editor.buffer.setPath(fullPath) + if editor.getPath()? + if filePath.length > 0 + editorPath = editor.getPath() + fullPath = getFullPath(filePath) + trySave(-> saveAs(fullPath)) + .then editor.buffer.setPath(editorPath) + .then deferred.resolve + else + trySave(-> editor.save()) + .then deferred.resolve + else + if filePath.length > 0 + fullPath = getFullPath(filePath) + trySave(-> saveAs(fullPath)) + .then -> editor.buffer.setPath(fullPath) + .then deferred.resolve + else + fullPath = atom.showSaveDialogSync() + if fullPath? + trySave(-> editor.saveAs(fullPath)) + .then -> editor.buffer.setPath(fullPath) + .then deferred.resolve deferred.promise From b5e9df10b4473eba8374f384443441394548b1a9 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 29 Jul 2015 16:38:05 +0200 Subject: [PATCH 018/131] Prepare 0.6.2 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b4cef3..c449a9f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.6.1", + "version": "0.6.2", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 42a44ee9e1fe92c8a02fb592c90df3d4afaf6769 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 29 Jul 2015 19:13:13 +0200 Subject: [PATCH 019/131] Add specs; minor changes to some commands `:tabedit` now works as an alias to `:edit` with a path and as an alias to `:tabnew` without. `:tabnew` is a new command that opens a new tab with a new file if used without a path and works as an alias to `:tabedit` with one. `:tabclose` now works as a proper alias to `:quit` (i.e. passes the arguments) `:edit` now works more like before - it opens a given path in a new tab. It also doesn't do anything if the file was modified since the last commit, unless forced by using `:edit!` `:write` works properly again and doesn't overwrite files, unless forced by using `:write!` `:xit` is now called `:xit` and not just `:x` `:substitute` now properly replaces multiple groups (`:s/(a)b(c)/X\1\2X\0`) --- ...ee => ex-normal-mode-input-element.coffee} | 0 lib/ex-state.coffee | 4 + lib/ex.coffee | 139 ++--- lib/view-model.coffee | 6 +- package.json | 3 + spec/ex-commands-spec.coffee | 518 ++++++++++++++++++ spec/ex-mode-spec.coffee | 62 --- spec/ex-mode-view-spec.coffee | 5 - spec/spec-helper.coffee | 65 +++ 9 files changed, 670 insertions(+), 132 deletions(-) rename lib/{ex-command-mode-input-element.coffee => ex-normal-mode-input-element.coffee} (100%) create mode 100644 spec/ex-commands-spec.coffee delete mode 100644 spec/ex-mode-spec.coffee delete mode 100644 spec/ex-mode-view-spec.coffee create mode 100644 spec/spec-helper.coffee diff --git a/lib/ex-command-mode-input-element.coffee b/lib/ex-normal-mode-input-element.coffee similarity index 100% rename from lib/ex-command-mode-input-element.coffee rename to lib/ex-normal-mode-input-element.coffee diff --git a/lib/ex-state.coffee b/lib/ex-state.coffee index b676535..7c0f37c 100644 --- a/lib/ex-state.coffee +++ b/lib/ex-state.coffee @@ -34,6 +34,9 @@ class ExState onDidFailToExecute: (fn) -> @emitter.on('failed-to-execute', fn) + onDidProcessOpStack: (fn) -> + @emitter.on('processed-op-stack', fn) + pushOperations: (operations) -> @opStack.push operations @@ -55,5 +58,6 @@ class ExState else throw e @clearOpStack() + @emitter.emit('processed-op-stack') module.exports = ExState diff --git a/lib/ex.coffee b/lib/ex.coffee index 7b7c349..e259c90 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -37,31 +37,32 @@ saveAs = (filePath) -> fs.writeFileSync(filePath, editor.getText()) getFullPath = (filePath) -> + filePath = fs.normalize(filePath) + if path.isAbsolute(filePath) - fullPath = filePath + filePath else if atom.project.getPaths().length == 0 - fullPath = path.join('~', filePath) + path.join(fs.normalize('~'), filePath) else - fullPath = path.join(atom.project.getPaths()[0], filePath) + path.join(atom.project.getPaths()[0], filePath) - return fs.normalize(fullPath) +replaceGroups = (groups, string) -> + replaced = '' + escaped = false + while (char = string[0])? + string = string[1..] + if char is '\\' and not escaped + escaped = true + else if /\d/.test(char) and escaped + escaped = false + group = groups[parseInt(char)] + group ?= '' + replaced += group + else + escaped = false + replaced += char -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') + replaced class Ex @singleton: => @@ -75,21 +76,21 @@ class Ex q: => @quit() - tabedit: (range, args) -> - args = args.trim() - filePaths = args.split(' ') - pane = atom.workspace.getActivePane() - if filePaths? and filePaths.length > 0 - for file in filePaths - do -> atom.workspace.openURIInPane file, pane + tabedit: (range, args) => + if args.trim() isnt '' + @edit(range, args) else - atom.workspace.openURIInPane('', pane) + @tabnew(range, args) tabe: (args...) => @tabedit(args...) - tabnew: (args...) => @tabedit(args...) + tabnew: (range, args) => + if args.trim() is '' + atom.workspace.open() + else + @tabedit(range, args) - tabclose: => @quit() + tabclose: (args...) => @quit(args...) tabc: => @tabclose() @@ -106,16 +107,30 @@ class Ex tabp: => @tabprevious() edit: (range, filePath) -> - filePath = fs.normalize(filePath.trim()) + filePath = filePath.trim() + if filePath[0] is '!' + force = true + filePath = filePath[1..].trim() + else + force = false + + editor = atom.workspace.getActiveTextEditor() + if editor.isModified() and not force + throw new CommandError('No write since last change (add ! to override)') if filePath.indexOf(' ') isnt -1 throw new CommandError('Only one file name allowed') - buffer = atom.workspace.getActiveTextEditor().buffer - if buffer.isModified() - throw new CommandError('Unsaved file') - if filePath is '' - filePath = buffer.getPath() - buffer.setPath(getFullPath(filePath)) - buffer.load() + + if filePath.length isnt 0 + fullPath = getFullPath(filePath) + if fullPath is editor.getPath() + editor.getBuffer().reload() + else + atom.workspace.open(fullPath) + else + if editor.getPath()? + editor.getBuffer().reload() + else + throw new CommandError('No file name') e: (args...) => @edit(args...) @@ -125,33 +140,33 @@ class Ex buffer.load() write: (range, filePath) -> + if filePath[0] is '!' + force = true + filePath = filePath[1..] + else + force = false + filePath = filePath.trim() + if filePath.indexOf(' ') isnt -1 + throw new CommandError('Only one file name allowed') + deferred = Promise.defer() - pane = atom.workspace.getActivePane() editor = atom.workspace.getActiveTextEditor() - if editor.getPath()? - if filePath.length > 0 - editorPath = editor.getPath() - fullPath = getFullPath(filePath) - trySave(-> saveAs(fullPath)) - .then editor.buffer.setPath(editorPath) - .then deferred.resolve - else - trySave(-> editor.save()) - .then deferred.resolve - else - if filePath.length > 0 - fullPath = getFullPath(filePath) - trySave(-> saveAs(fullPath)) - .then -> editor.buffer.setPath(fullPath) - .then deferred.resolve - else - fullPath = atom.showSaveDialogSync() - if fullPath? - trySave(-> editor.saveAs(fullPath)) - .then -> editor.buffer.setPath(fullPath) - .then deferred.resolve + saved = false + if filePath.length isnt 0 + fullPath = getFullPath(filePath) + if editor.getPath()? and (not fullPath? or editor.getPath() == fullPath) + # Use editor.save when no path is given or the path to the file is given + trySave(-> editor.save()).then(deferred.resolve) + saved = true + else if not fullPath? + fullPath = atom.showSaveDialogSync() + + if not saved and fullPath? + if not force and fs.existsSync(fullPath) + throw new CommandError("File exists (add ! to override)") + trySave(-> saveAs(fullPath)).then(deferred.resolve) deferred.promise @@ -161,7 +176,7 @@ class Ex wq: (args...) => @write(args...).then => @quit() - x: (args...) => @wq(args...) + xit: (args...) => @wq(args...) wa: -> atom.workspace.saveAll() diff --git a/lib/view-model.coffee b/lib/view-model.coffee index af96e77..742d751 100644 --- a/lib/view-model.coffee +++ b/lib/view-model.coffee @@ -1,11 +1,11 @@ -ExCommandModeInputElement = require './ex-command-mode-input-element' +ExNormalModeInputElement = require './ex-normal-mode-input-element' class ViewModel constructor: (@command, opts={}) -> {@editor, @exState} = @command - @view = new ExCommandModeInputElement().initialize(@, opts) - @editor.commandModeInputView = @view + @view = new ExNormalModeInputElement().initialize(@, opts) + @editor.normalModeInputView = @view @exState.onDidFailToExecute => @view.remove() @done = false diff --git a/package.json b/package.json index c449a9f..5b986cb 100644 --- a/package.json +++ b/package.json @@ -32,5 +32,8 @@ "0.20.0": "provideEx" } } + }, + "devDependencies": { + "node-uuid": "^1.4.2" } } diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee new file mode 100644 index 0000000..803aa57 --- /dev/null +++ b/spec/ex-commands-spec.coffee @@ -0,0 +1,518 @@ +fs = require 'fs-plus' +path = require 'path' +os = require 'os' +uuid = require 'node-uuid' +helpers = require './spec-helper' + +Ex = require('../lib/ex').singleton() + +describe "the commands", -> + [editor, editorElement, vimState, exState, dir, dir2] = [] + projectPath = (fileName) -> path.join(dir, fileName) + beforeEach -> + vimMode = atom.packages.loadPackage('vim-mode') + exMode = atom.packages.loadPackage('ex-mode') + exMode.activate() + + waitsForPromise -> + vimMode.activate().then -> + helpers.activateExMode().then -> + dir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") + dir2 = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") + fs.makeTreeSync(dir) + fs.makeTreeSync(dir2) + atom.project.setPaths([dir, dir2]) + + helpers.getEditorElement (element) -> + atom.commands.dispatch(element, 'ex-mode:open') + keydown('escape') + editorElement = element + editor = editorElement.getModel() + vimState = vimMode.mainModule.getEditorState(editor) + exState = exMode.mainModule.exStates.get(editor) + vimState.activateNormalMode() + vimState.resetNormalMode() + editor.setText("abc\ndef\nabc\ndef") + + afterEach -> + fs.removeSync(dir) + fs.removeSync(dir2) + + keydown = (key, options={}) -> + options.element ?= editorElement + helpers.keydown(key, options) + + normalModeInputKeydown = (key, opts = {}) -> + editor.normalModeInputView.editorElement.getModel().setText(key) + + submitNormalModeInputText = (text) -> + commandEditor = editor.normalModeInputView.editorElement + commandEditor.getModel().setText(text) + atom.commands.dispatch(commandEditor, "core:confirm") + + describe ":write", -> + describe "when editing a new file", -> + beforeEach -> + editor.getBuffer().setText('abc\ndef') + + it "opens the save dialog", -> + spyOn(atom, 'showSaveDialogSync') + keydown(':') + submitNormalModeInputText('write') + expect(atom.showSaveDialogSync).toHaveBeenCalled() + + it "saves when a path is specified in the save dialog", -> + filePath = projectPath('write-from-save-dialog') + spyOn(atom, 'showSaveDialogSync').andReturn(filePath) + keydown(':') + submitNormalModeInputText('write') + expect(fs.existsSync(filePath)).toBe(true) + expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc\ndef') + + it "saves when a path is specified in the save dialog", -> + spyOn(atom, 'showSaveDialogSync').andReturn(undefined) + spyOn(fs, 'writeFileSync') + keydown(':') + submitNormalModeInputText('write') + expect(fs.writeFileSync.calls.length).toBe(0) + + describe "when editing an existing file", -> + filePath = '' + i = 0 + + beforeEach -> + i++ + filePath = projectPath("write-#{i}") + editor.setText('abc\ndef') + editor.saveAs(filePath) + + it "saves the file", -> + editor.setText('abc') + keydown(':') + submitNormalModeInputText('write') + expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc') + expect(editor.isModified()).toBe(false) + + describe "with a specified path", -> + newPath = '' + + beforeEach -> + newPath = path.relative(dir, "#{filePath}.new") + editor.getBuffer().setText('abc') + keydown(':') + + afterEach -> + submitNormalModeInputText("write #{newPath}") + newPath = path.resolve(dir, fs.normalize(newPath)) + expect(fs.existsSync(newPath)).toBe(true) + expect(fs.readFileSync(newPath, 'utf-8')).toEqual('abc') + expect(editor.isModified()).toBe(true) + fs.removeSync(newPath) + + it "saves to the path", -> + + it "expands .", -> + newPath = path.join('.', newPath) + + it "expands ..", -> + newPath = path.join('..', newPath) + + it "expands ~", -> + newPath = path.join('~', newPath) + + it "throws an error with more than one path", -> + keydown(':') + submitNormalModeInputText('write path1 path2') + expect(atom.notifications.notifications[0].message).toEqual( + 'Command error: Only one file name allowed' + ) + + describe "when the file already exists", -> + existsPath = '' + + beforeEach -> + existsPath = projectPath('write-exists') + fs.writeFileSync(existsPath, 'abc') + + afterEach -> + fs.removeSync(existsPath) + + it "throws an error if the file already exists", -> + keydown(':') + submitNormalModeInputText("write #{existsPath}") + expect(atom.notifications.notifications[0].message).toEqual( + 'Command error: File exists (add ! to override)' + ) + expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc') + + it "writes if forced with :write!", -> + keydown(':') + submitNormalModeInputText("write! #{existsPath}") + expect(atom.notifications.notifications).toEqual([]) + expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef') + + describe ":quit", -> + pane = null + beforeEach -> + waitsForPromise -> + pane = atom.workspace.getActivePane() + spyOn(pane, 'destroyActiveItem').andCallThrough() + atom.workspace.open() + + it "closes the active pane item if not modified", -> + keydown(':') + submitNormalModeInputText('quit') + expect(pane.destroyActiveItem).toHaveBeenCalled() + expect(pane.getItems().length).toBe(1) + + describe "when the active pane item is modified", -> + beforeEach -> + editor.getBuffer().setText('def') + + it "opens the prompt to save", -> + spyOn(pane, 'promptToSaveItem') + keydown(':') + submitNormalModeInputText('quit') + expect(pane.promptToSaveItem).toHaveBeenCalled() + + describe ":tabclose", -> + it "acts as an alias to :quit", -> + spyOn(Ex, 'tabclose').andCallThrough() + spyOn(Ex, 'quit').andCallThrough() + keydown(':') + submitNormalModeInputText('tabclose') + expect(Ex.quit).toHaveBeenCalledWith(Ex.tabclose.calls[0].args...) + + describe ":tabnext", -> + pane = null + beforeEach -> + waitsForPromise -> + pane = atom.workspace.getActivePane() + atom.workspace.open().then -> atom.workspace.open() + .then -> atom.workspace.open() + + it "switches to the next tab", -> + pane.activateItemAtIndex(1) + keydown(':') + submitNormalModeInputText('tabnext') + expect(pane.getActiveItemIndex()).toBe(2) + + it "wraps around", -> + pane.activateItemAtIndex(pane.getItems().length - 1) + keydown(':') + submitNormalModeInputText('tabnext') + expect(pane.getActiveItemIndex()).toBe(0) + + describe ":tabprevious", -> + pane = null + beforeEach -> + waitsForPromise -> + pane = atom.workspace.getActivePane() + atom.workspace.open().then -> atom.workspace.open() + .then -> atom.workspace.open() + + it "switches to the previous tab", -> + pane.activateItemAtIndex(1) + keydown(':') + submitNormalModeInputText('tabprevious') + expect(pane.getActiveItemIndex()).toBe(0) + + it "wraps around", -> + pane.activateItemAtIndex(0) + keydown(':') + submitNormalModeInputText('tabprevious') + expect(pane.getActiveItemIndex()).toBe(pane.getItems().length - 1) + + describe ":wq", -> + beforeEach -> + spyOn(Ex, 'write').andCallThrough() + spyOn(Ex, 'quit') + + it "writes the file, then quits", -> + spyOn(atom, 'showSaveDialogSync').andReturn(projectPath('wq-1')) + keydown(':') + submitNormalModeInputText('wq') + expect(Ex.write).toHaveBeenCalled() + # Since `:wq` only calls `:quit` after `:write` is finished, we need to + # wait a bit for the `:quit` call to occur + waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100) + + it "doesn't quit when the file is new and no path is specified in the save dialog", -> + spyOn(atom, 'showSaveDialogSync').andReturn(undefined) + keydown(':') + submitNormalModeInputText('wq') + expect(Ex.write).toHaveBeenCalled() + wasNotCalled = false + # FIXME: This seems dangerous, but setTimeout somehow doesn't work. + setImmediate((-> + wasNotCalled = not Ex.quit.wasCalled)) + waitsFor((-> wasNotCalled), 100) + + it "passes the file name", -> + keydown(':') + submitNormalModeInputText('wq wq-2') + expect(Ex.write) + .toHaveBeenCalled() + expect(Ex.write.calls[0].args[1].trim()).toEqual('wq-2') + waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100) + + describe ":xit", -> + it "acts as an alias to :wq", -> + spyOn(Ex, 'wq') + keydown(':') + submitNormalModeInputText('xit') + expect(Ex.wq).toHaveBeenCalled() + + describe ":edit", -> + describe "without a file name", -> + it "reloads the file from the disk", -> + filePath = projectPath("edit-1") + editor.getBuffer().setText('abc') + editor.saveAs(filePath) + fs.writeFileSync(filePath, 'def') + keydown(':') + submitNormalModeInputText('edit') + # Reloading takes a bit + waitsFor((-> editor.getText() is 'def'), + "the editor's content to change", 100) + + it "doesn't reload when the file has been modified", -> + filePath = projectPath("edit-2") + editor.getBuffer().setText('abc') + editor.saveAs(filePath) + editor.getBuffer().setText('abcd') + fs.writeFileSync(filePath, 'def') + keydown(':') + submitNormalModeInputText('edit') + expect(atom.notifications.notifications[0].message).toEqual( + 'Command error: No write since last change (add ! to override)') + isntDef = false + setImmediate(-> isntDef = editor.getText() isnt 'def') + waitsFor((-> isntDef), "the editor's content not to change", 50) + + it "reloads when the file has been modified and it is forced", -> + filePath = projectPath("edit-3") + editor.getBuffer().setText('abc') + editor.saveAs(filePath) + editor.getBuffer().setText('abcd') + fs.writeFileSync(filePath, 'def') + keydown(':') + submitNormalModeInputText('edit!') + expect(atom.notifications.notifications.length).toBe(0) + waitsFor((-> editor.getText() is 'def') + "the editor's content to change", 50) + + it "throws an error when editing a new file", -> + editor.getBuffer().reload() + keydown(':') + submitNormalModeInputText('edit') + expect(atom.notifications.notifications[0].message).toEqual( + 'Command error: No file name') + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText('edit!') + expect(atom.notifications.notifications[1].message).toEqual( + 'Command error: No file name') + + describe "with a file name", -> + beforeEach -> + spyOn(atom.workspace, 'open') + editor.getBuffer().reload() + + it "opens the specified path", -> + filePath = projectPath('edit-new-test') + keydown(':') + submitNormalModeInputText("edit #{filePath}") + expect(atom.workspace.open).toHaveBeenCalledWith(filePath) + + it "opens a relative path", -> + keydown(':') + submitNormalModeInputText('edit edit-relative-test') + expect(atom.workspace.open).toHaveBeenCalledWith( + projectPath('edit-relative-test')) + + it "throws an error if trying to open more than one file", -> + keydown(':') + submitNormalModeInputText('edit edit-new-test-1 edit-new-test-2') + expect(atom.workspace.open.callCount).toBe(0) + expect(atom.notifications.notifications[0].message).toEqual( + 'Command error: Only one file name allowed') + + describe ":tabedit", -> + it "acts as an alias to :edit if supplied with a path", -> + spyOn(Ex, 'tabedit').andCallThrough() + spyOn(Ex, 'edit') + keydown(':') + submitNormalModeInputText('tabedit tabedit-test') + expect(Ex.edit).toHaveBeenCalledWith(Ex.tabedit.calls[0].args...) + + it "acts as an alias to :tabnew if not supplied with a path", -> + spyOn(Ex, 'tabedit').andCallThrough() + spyOn(Ex, 'tabnew') + keydown(':') + submitNormalModeInputText('tabedit ') + expect(Ex.tabnew) + .toHaveBeenCalledWith(Ex.tabedit.calls[0].args...) + + describe ":tabnew", -> + it "opens a new tab", -> + spyOn(atom.workspace, 'open') + keydown(':') + submitNormalModeInputText('tabnew') + expect(atom.workspace.open).toHaveBeenCalled() + + describe ":split", -> + it "splits the current file upwards", -> + pane = atom.workspace.getActivePane() + spyOn(pane, 'splitUp').andCallThrough() + filePath = projectPath('split') + editor.saveAs(filePath) + keydown(':') + submitNormalModeInputText('split') + expect(pane.splitUp).toHaveBeenCalled() + # FIXME: Should test whether the new pane contains a TextEditor + # pointing to the same path + + describe ":vsplit", -> + it "splits the current file to the left", -> + pane = atom.workspace.getActivePane() + spyOn(pane, 'splitLeft').andCallThrough() + filePath = projectPath('vsplit') + editor.saveAs(filePath) + keydown(':') + submitNormalModeInputText('vsplit') + expect(pane.splitLeft).toHaveBeenCalled() + # FIXME: Should test whether the new pane contains a TextEditor + # pointing to the same path + + describe ":delete", -> + beforeEach -> + editor.setText('abc\ndef\nghi\njkl') + editor.setCursorBufferPosition([2, 0]) + + it "deletes the current line", -> + keydown(':') + submitNormalModeInputText('delete') + expect(editor.getText()).toEqual('abc\ndef\njkl') + + it "deletes the lines in the given range", -> + processedOpStack = false + exState.onDidProcessOpStack -> processedOpStack = true + keydown(':') + submitNormalModeInputText('1,2delete') + expect(editor.getText()).toEqual('ghi\njkl') + + waitsFor -> processedOpStack + editor.setText('abc\ndef\nghi\njkl') + editor.setCursorBufferPosition([1, 1]) + # For some reason, keydown(':') doesn't work here :/ + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(',/k/delete') + expect(editor.getText()).toEqual('abc\n') + + it "undos deleting several lines at once", -> + keydown(':') + submitNormalModeInputText('-1,.delete') + expect(editor.getText()).toEqual('abc\njkl') + atom.commands.dispatch(editorElement, 'core:undo') + expect(editor.getText()).toEqual('abc\ndef\nghi\njkl') + + describe ":substitute", -> + beforeEach -> + editor.setText('abcaABC\ndefdDEF\nabcaABC') + editor.setCursorBufferPosition([0, 0]) + + it "replaces a character on the current line", -> + keydown(':') + submitNormalModeInputText(':substitute /a/x') + expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC') + + it "doesn't need a space before the arguments", -> + keydown(':') + submitNormalModeInputText(':substitute/a/x') + expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC') + + it "respects modifiers passed to it", -> + keydown(':') + submitNormalModeInputText(':substitute/a/x/g') + expect(editor.getText()).toEqual('xbcxABC\ndefdDEF\nabcaABC') + + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':substitute/a/x/gi') + expect(editor.getText()).toEqual('xbcxxBC\ndefdDEF\nabcaABC') + + it "replaces on multiple lines", -> + keydown(':') + submitNormalModeInputText(':%substitute/abc/ghi') + expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nghiaABC') + + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':%substitute/abc/ghi/ig') + expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nghiaghi') + + it "can't be delimited by letters", -> + keydown(':') + submitNormalModeInputText(':substitute nanxngi') + expect(atom.notifications.notifications[0].message).toEqual( + "Command error: Regular expressions can't be delimited by letters") + expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC') + + describe "capturing groups", -> + beforeEach -> + editor.setText('abcaABC\ndefdDEF\nabcaABC') + + it "replaces \\1 with the first group", -> + keydown(':') + submitNormalModeInputText(':substitute/bc(.{2})/X\\1X') + expect(editor.getText()).toEqual('aXaAXBC\ndefdDEF\nabcaABC') + + it "replaces multiple groups", -> + keydown(':') + submitNormalModeInputText(':substitute/a([a-z]*)aA([A-Z]*)/X\\1XY\\2Y') + expect(editor.getText()).toEqual('XbcXYBCY\ndefdDEF\nabcaABC') + + it "replaces \\0 with the entire match", -> + keydown(':') + submitNormalModeInputText(':substitute/ab(ca)AB/X\\0X') + expect(editor.getText()).toEqual('XabcaABXC\ndefdDEF\nabcaABC') + + describe ":set", -> + it "throws an error without a specified option", -> + keydown(':') + submitNormalModeInputText(':set') + expect(atom.notifications.notifications[0].message).toEqual( + 'Command error: No option specified') + + it "sets multiple options at once", -> + atom.config.set('editor.showInvisibles', false) + atom.config.set('editor.showLineNumbers', false) + keydown(':') + submitNormalModeInputText(':set list number') + expect(atom.config.get('editor.showInvisibles')).toBe(true) + expect(atom.config.get('editor.showLineNumbers')).toBe(true) + + describe "the options", -> + beforeEach -> + atom.config.set('editor.showInvisibles', false) + atom.config.set('editor.showLineNumbers', false) + + it "sets (no)list", -> + keydown(':') + submitNormalModeInputText(':set list') + expect(atom.config.get('editor.showInvisibles')).toBe(true) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set nolist') + expect(atom.config.get('editor.showInvisibles')).toBe(false) + + it "sets (no)nu(mber)", -> + keydown(':') + submitNormalModeInputText(':set nu') + expect(atom.config.get('editor.showLineNumbers')).toBe(true) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set nonu') + expect(atom.config.get('editor.showLineNumbers')).toBe(false) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set number') + expect(atom.config.get('editor.showLineNumbers')).toBe(true) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set nonumber') + expect(atom.config.get('editor.showLineNumbers')).toBe(false) diff --git a/spec/ex-mode-spec.coffee b/spec/ex-mode-spec.coffee deleted file mode 100644 index bbc7fd7..0000000 --- a/spec/ex-mode-spec.coffee +++ /dev/null @@ -1,62 +0,0 @@ -ExMode = require '../lib/ex-mode' - -# Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. -# -# To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` -# or `fdescribe`). Remove the `f` to unfocus the block. - -describe "ExMode", -> - [workspaceElement, activationPromise] = [] - - beforeEach -> - workspaceElement = atom.views.getView(atom.workspace) - activationPromise = atom.packages.activatePackage('ex-mode') - - describe "when the ex-mode:toggle event is triggered", -> - it "hides and shows the modal panel", -> - # Before the activation event the view is not on the DOM, and no panel - # has been created - expect(workspaceElement.querySelector('.ex-mode')).not.toExist() - - # This is an activation event, triggering it will cause the package to be - # activated. - atom.commands.dispatch workspaceElement, 'ex-mode:toggle' - - waitsForPromise -> - activationPromise - - runs -> - expect(workspaceElement.querySelector('.ex-mode')).toExist() - - exModeElement = workspaceElement.querySelector('.ex-mode') - expect(exModeElement).toExist() - - exModePanel = atom.workspace.panelForItem(exModeElement) - expect(exModePanel.isVisible()).toBe true - atom.commands.dispatch workspaceElement, 'ex-mode:toggle' - expect(exModePanel.isVisible()).toBe false - - it "hides and shows the view", -> - # This test shows you an integration test testing at the view level. - - # Attaching the workspaceElement to the DOM is required to allow the - # `toBeVisible()` matchers to work. Anything testing visibility or focus - # requires that the workspaceElement is on the DOM. Tests that attach the - # workspaceElement to the DOM are generally slower than those off DOM. - jasmine.attachToDOM(workspaceElement) - - expect(workspaceElement.querySelector('.ex-mode')).not.toExist() - - # This is an activation event, triggering it causes the package to be - # activated. - atom.commands.dispatch workspaceElement, 'ex-mode:toggle' - - waitsForPromise -> - activationPromise - - runs -> - # Now we can test for view visibility - exModeElement = workspaceElement.querySelector('.ex-mode') - expect(exModeElement).toBeVisible() - atom.commands.dispatch workspaceElement, 'ex-mode:toggle' - expect(exModeElement).not.toBeVisible() diff --git a/spec/ex-mode-view-spec.coffee b/spec/ex-mode-view-spec.coffee deleted file mode 100644 index d94ddf1..0000000 --- a/spec/ex-mode-view-spec.coffee +++ /dev/null @@ -1,5 +0,0 @@ -ExModeView = require '../lib/ex-mode-view' - -describe "ExModeView", -> - it "has one valid test", -> - expect("life").toBe "easy" diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee new file mode 100644 index 0000000..9ec4d58 --- /dev/null +++ b/spec/spec-helper.coffee @@ -0,0 +1,65 @@ +ExState = require '../lib/ex-state' +GlobalExState = require '../lib/global-ex-state' + +beforeEach -> + atom.workspace ||= {} + +activateExMode = -> + atom.workspace.open().then -> + atom.commands.dispatch(atom.views.getView(atom.workspace), 'ex-mode:open') + keydown('escape') + atom.workspace.getActivePane().destroyActiveItem() + + +getEditorElement = (callback) -> + textEditor = null + + waitsForPromise -> + atom.workspace.open().then (e) -> + textEditor = e + + runs -> + # element = document.createElement("atom-text-editor") + # element.setModel(textEditor) + # element.classList.add('vim-mode') + # element.exState = new ExState(element, new GlobalExState) + # + # element.addEventListener "keydown", (e) -> + # atom.keymaps.handleKeyboardEvent(e) + + element = atom.views.getView(textEditor) + + callback(element) + +dispatchKeyboardEvent = (target, eventArgs...) -> + e = document.createEvent('KeyboardEvent') + e.initKeyboardEvent(eventArgs...) + # 0 is the default, and it's valid ASCII, but it's wrong. + Object.defineProperty(e, 'keyCode', get: -> undefined) if e.keyCode is 0 + target.dispatchEvent e + +dispatchTextEvent = (target, eventArgs...) -> + e = document.createEvent('TextEvent') + e.initTextEvent(eventArgs...) + target.dispatchEvent e + +keydown = (key, {element, ctrl, shift, alt, meta, raw}={}) -> + key = "U+#{key.charCodeAt(0).toString(16)}" unless key is 'escape' or raw? + element ||= document.activeElement + eventArgs = [ + true, # bubbles + true, # cancelable + null, # view + key, # key + 0, # location + ctrl, alt, shift, meta + ] + + canceled = not dispatchKeyboardEvent(element, 'keydown', eventArgs...) + dispatchKeyboardEvent(element, 'keypress', eventArgs...) + if not canceled + if dispatchTextEvent(element, 'textInput', eventArgs...) + element.value += key + dispatchKeyboardEvent(element, 'keyup', eventArgs...) + +module.exports = {keydown, getEditorElement, activateExMode} From 59fb0ddf1f31f296e22f74d6a6aa346e9a3c4871 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 30 Jul 2015 08:50:36 +0200 Subject: [PATCH 020/131] enable Travis CI --- .travis.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d73c8e2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: objective-c + +notifications: + email: + on_success: never + on_failure: change + +script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' + +git: + depth: 10 From 962e4a35bad18cdd2e9a46471440c6589272269b Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 30 Jul 2015 08:55:14 +0200 Subject: [PATCH 021/131] travis: install vim-mode --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index d73c8e2..3a85896 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: objective-c +env: + - APM_TEST_PACKAGES="vim-mode" notifications: email: From d0cbbb5d15f9ae48c0bfb8197db96159247bc87b Mon Sep 17 00:00:00 2001 From: Polo Date: Sat, 1 Aug 2015 15:29:56 +0200 Subject: [PATCH 022/131] Make cmd-line in ex-mode look like in vim-mode Changed the style of .command-mode-input so that it looks like .normal-mode-input from vim-mode. This makes ex-mode more consistent with vim-mode --- styles/ex-mode.less | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/styles/ex-mode.less b/styles/ex-mode.less index fea8230..ee0e557 100644 --- a/styles/ex-mode.less +++ b/styles/ex-mode.less @@ -10,3 +10,15 @@ div[is=ex-command-mode-input] atom-text-editor[mini]::before { content: ":"; } + +.command-mode-input atom-text-editor[mini] { + background-color: inherit; + border: none; + width: 100%; + font-weight: normal; + color: @text-color; + line-height: 1.28; + cursor: default; + white-space: nowrap; + padding-left: 10px; +} From 72d80ed4e90b3f80948ae5f5c6bbb8cf1980eb96 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Mon, 3 Aug 2015 12:20:26 +0200 Subject: [PATCH 023/131] prefix vim-mode's search --- styles/ex-mode.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/styles/ex-mode.less b/styles/ex-mode.less index ee0e557..4d24ae4 100644 --- a/styles/ex-mode.less +++ b/styles/ex-mode.less @@ -11,6 +11,10 @@ div[is=ex-command-mode-input] atom-text-editor[mini]::before { content: ":"; } +div[is=vim-normal-mode-input] atom-text-editor[mini]::before { + content: "/"; +} + .command-mode-input atom-text-editor[mini] { background-color: inherit; border: none; From c0c220c22ef6ffcf04914de710bbcb2a4f19aa2b Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Mon, 3 Aug 2015 12:21:01 +0200 Subject: [PATCH 024/131] distinguish prefix from input --- styles/ex-mode.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/styles/ex-mode.less b/styles/ex-mode.less index 4d24ae4..2a714b0 100644 --- a/styles/ex-mode.less +++ b/styles/ex-mode.less @@ -9,10 +9,12 @@ div[is=ex-command-mode-input] atom-text-editor[mini]::before { content: ":"; + opacity: 0.5; } div[is=vim-normal-mode-input] atom-text-editor[mini]::before { content: "/"; + opacity: 0.5; } .command-mode-input atom-text-editor[mini] { From 728ccaa5f923fbf763d1c329704ee74b92c8d564 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Mon, 3 Aug 2015 12:21:31 +0200 Subject: [PATCH 025/131] Prepare 0.7.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b986cb..90e6b31 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.6.2", + "version": "0.7.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From e2841dc26cb0918528b622a6d0994984a75fde95 Mon Sep 17 00:00:00 2001 From: Jacob Wahlgren Date: Tue, 22 Sep 2015 00:35:36 +0200 Subject: [PATCH 026/131] Don't allow :s delimiters not allowed by vim "Instead of the '/' which surrounds the pattern and replacement string, you can use any other single-byte character, but not an alphanumeric character, '\', '"'' or '|'." - http://vimdoc.sourceforge.net/htmldoc/change.html#:substitute --- lib/ex.coffee | 4 ++-- spec/ex-commands-spec.coffee | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index e259c90..684b2f6 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -199,9 +199,9 @@ class Ex substitute: (range, args) -> args = args.trimLeft() delim = args[0] - if /[a-z]/i.test(delim) + if /[a-z1-9\\"|]/i.test(delim) throw new CommandError( - "Regular expressions can't be delimited by letters") + "Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") delimRE = new RegExp("[^\\\\]#{delim}") spl = [] args_ = args[1..] diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 803aa57..7bf10af 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -453,7 +453,14 @@ describe "the commands", -> keydown(':') submitNormalModeInputText(':substitute nanxngi') expect(atom.notifications.notifications[0].message).toEqual( - "Command error: Regular expressions can't be delimited by letters") + "Command error: Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") + expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC') + + it "can't be delimited by numbers", -> + keydown(':') + submitNormalModeInputText(':substitute 1a1x1gi') + expect(atom.notifications.notifications[0].message).toEqual( + "Command error: Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC') describe "capturing groups", -> From 77d3fa46d53ab7db4deedd5936940eb7e98f5c68 Mon Sep 17 00:00:00 2001 From: Jacob Wahlgren Date: Tue, 22 Sep 2015 00:50:21 +0200 Subject: [PATCH 027/131] Refactor illegal delimiters specs --- spec/ex-commands-spec.coffee | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 7bf10af..a4813b5 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -449,19 +449,19 @@ describe "the commands", -> submitNormalModeInputText(':%substitute/abc/ghi/ig') expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nghiaghi') - it "can't be delimited by letters", -> - keydown(':') - submitNormalModeInputText(':substitute nanxngi') - expect(atom.notifications.notifications[0].message).toEqual( - "Command error: Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") - expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC') + describe "illegal delimiters", -> + test = (delim) -> + keydown(':') + submitNormalModeInputText(":substitute #{delim}a#{delim}x#{delim}gi") + expect(atom.notifications.notifications[0].message).toEqual( + "Command error: Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") + expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC') - it "can't be delimited by numbers", -> - keydown(':') - submitNormalModeInputText(':substitute 1a1x1gi') - expect(atom.notifications.notifications[0].message).toEqual( - "Command error: Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") - expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC') + it "can't be delimited by letters", -> test 'n' + it "can't be delimited by numbers", -> test '3' + it "can't be delimited by '\\'", -> test '\\' + it "can't be delimited by '\"'", -> test '"' + it "can't be delimited by '|'", -> test '|' describe "capturing groups", -> beforeEach -> From e0ee339bf64f29bd8922d06b04794c2842d2fc91 Mon Sep 17 00:00:00 2001 From: Alexey Shamrin Date: Sat, 31 Oct 2015 05:33:03 +0300 Subject: [PATCH 028/131] backspace over empty `:` now cancels ex-mode fixes #108 --- lib/ex-normal-mode-input-element.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/ex-normal-mode-input-element.coffee b/lib/ex-normal-mode-input-element.coffee index 91e0eb2..73ca3da 100644 --- a/lib/ex-normal-mode-input-element.coffee +++ b/lib/ex-normal-mode-input-element.coffee @@ -36,11 +36,16 @@ class ExCommandModeInputElement extends HTMLDivElement @confirm() if e.newText else atom.commands.add(@editorElement, 'editor:newline', @confirm.bind(this)) + atom.commands.add(@editorElement, 'core:backspace', @backspace.bind(this)) atom.commands.add(@editorElement, 'core:confirm', @confirm.bind(this)) atom.commands.add(@editorElement, 'core:cancel', @cancel.bind(this)) atom.commands.add(@editorElement, 'blur', @cancel.bind(this)) + backspace: -> + # pressing backspace over empty `:` should cancel ex-mode + @cancel() unless @editorElement.getModel().getText().length + confirm: -> @value = @editorElement.getModel().getText() or @defaultText @viewModel.confirm(this) From 14d234d1827c3fbd01401fe0db7bb3ebbe904721 Mon Sep 17 00:00:00 2001 From: Gertjan Reynaert Date: Tue, 17 Nov 2015 16:39:21 +0100 Subject: [PATCH 029/131] Add option to register aliasses --- lib/ex.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/ex.coffee b/lib/ex.coffee index 684b2f6..71fc153 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -71,6 +71,9 @@ class Ex @registerCommand: (name, func) => @singleton()[name] = func + @registerAlias: (alias, name) => + @singleton()[alias] = @singleton()[name] + quit: -> atom.workspace.getActivePane().destroyActiveItem() From 472ec2140e80f7e1852c0cff9fda5a1d0e878be3 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Thu, 19 Nov 2015 14:42:29 +0100 Subject: [PATCH 030/131] looking for new maintainer --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b3084de..abf3f64 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ ex-mode for Atom's vim-mode +**I now only rarely use Atom and am looking for a new maintainer for this project. Please chime in [here](https://github.com/lloeki/ex-mode/issues/116).** + ## Use Install both [vim-mode](https://github.com/atom/vim-mode) and ex-mode. Type `:` in command mode. Enter `w` or `write`. From af0ba7c01c1fdc4a9c21c92dc39b878642e9c492 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sat, 21 Nov 2015 14:48:13 +0100 Subject: [PATCH 031/131] Improve format for calling commands Commands (from the Ex class) are now called with an object containing the range, arguments, vim state, ex state and editor instead of a long list of arguments. --- lib/command.coffee | 4 +-- lib/ex.coffee | 55 ++++++++++++++++++------------------ spec/ex-commands-spec.coffee | 48 ++++++++++++++++++------------- 3 files changed, 58 insertions(+), 49 deletions(-) diff --git a/lib/command.coffee b/lib/command.coffee index 115b273..172cefb 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -150,7 +150,7 @@ class Command # If the command matches an existing one exactly, execute that one if (func = Ex.singleton()[command])? - func(range, args) + func({ range, args, @vimState, @exState, @editor }) else # Step 8: Match command against existing commands matching = (name for name, val of Ex.singleton() when \ @@ -162,7 +162,7 @@ class Command func = Ex.singleton()[command] if func? - func(range, args) + func({ range, args, @vimState, @exState, @editor }) else throw new CommandError("Not an editor command: #{input.characters}") diff --git a/lib/ex.coffee b/lib/ex.coffee index 684b2f6..25d3b7e 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -32,8 +32,7 @@ trySave = (func) -> deferred.promise -saveAs = (filePath) -> - editor = atom.workspace.getActiveTextEditor() +saveAs = (filePath, editor) -> fs.writeFileSync(filePath, editor.getText()) getFullPath = (filePath) -> @@ -76,21 +75,21 @@ class Ex q: => @quit() - tabedit: (range, args) => - if args.trim() isnt '' - @edit(range, args) + tabedit: (args) => + if args.args.trim() isnt '' + @edit(args) else - @tabnew(range, args) + @tabnew(args) - tabe: (args...) => @tabedit(args...) + tabe: (args) => @tabedit(args) - tabnew: (range, args) => + tabnew: ({ range, args }) => if args.trim() is '' atom.workspace.open() else @tabedit(range, args) - tabclose: (args...) => @quit(args...) + tabclose: (args) => @quit(args) tabc: => @tabclose() @@ -106,15 +105,14 @@ class Ex tabp: => @tabprevious() - edit: (range, filePath) -> - filePath = filePath.trim() + edit: ({ range, args, editor }) -> + filePath = args.trim() if filePath[0] is '!' force = true filePath = filePath[1..].trim() else force = false - editor = atom.workspace.getActiveTextEditor() if editor.isModified() and not force throw new CommandError('No write since last change (add ! to override)') if filePath.indexOf(' ') isnt -1 @@ -132,14 +130,15 @@ class Ex else throw new CommandError('No file name') - e: (args...) => @edit(args...) + e: (args) => @edit(args) enew: -> buffer = atom.workspace.getActiveTextEditor().buffer buffer.setPath(undefined) buffer.load() - write: (range, filePath) -> + write: ({ range, args, editor }) -> + filePath = args if filePath[0] is '!' force = true filePath = filePath[1..] @@ -166,22 +165,22 @@ class Ex if not saved and fullPath? if not force and fs.existsSync(fullPath) throw new CommandError("File exists (add ! to override)") - trySave(-> saveAs(fullPath)).then(deferred.resolve) + trySave(-> saveAs(fullPath, editor)).then(deferred.resolve) deferred.promise - w: (args...) => - @write(args...) + w: (args) => + @write(args) - wq: (args...) => - @write(args...).then => @quit() + wq: (args) => + @write(args).then => @quit() - xit: (args...) => @wq(args...) + xit: (args) => @wq(args) wa: -> atom.workspace.saveAll() - split: (range, args) -> + split: ({ range, args }) -> args = args.trim() filePaths = args.split(' ') filePaths = undefined if filePaths.length is 1 and filePaths[0] is '' @@ -194,9 +193,9 @@ class Ex else pane.splitUp(copyActiveItem: true) - sp: (args...) => @split(args...) + sp: (args) => @split(args) - substitute: (range, args) -> + substitute: ({ range, args, editor, vimState }) -> args = args.trimLeft() delim = args[0] if /[a-z1-9\\"|]/i.test(delim) @@ -240,9 +239,9 @@ class Ex replace(replaceGroups(match[..], spl[1])) ) - s: (args...) => @substitute(args...) + s: (args) => @substitute(args) - vsplit: (range, args) -> + vsplit: ({ range, args }) -> args = args.trim() filePaths = args.split(' ') filePaths = undefined if filePaths.length is 1 and filePaths[0] is '' @@ -255,13 +254,13 @@ class Ex else pane.splitLeft(copyActiveItem: true) - vsp: (args...) => @vsplit(args...) + vsp: (args) => @vsplit(args) - delete: (range) -> + delete: ({ range }) -> range = [[range[0], 0], [range[1] + 1, 0]] atom.workspace.getActiveTextEditor().buffer.setTextInRange(range, '') - set: (range, args) -> + set: ({ range, args }) -> args = args.trim() if args == "" throw new CommandError("No option specified") diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index a4813b5..6fd02c3 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -12,27 +12,37 @@ describe "the commands", -> beforeEach -> vimMode = atom.packages.loadPackage('vim-mode') exMode = atom.packages.loadPackage('ex-mode') - exMode.activate() + waitsForPromise -> + activationPromise = exMode.activate() + helpers.activateExMode() + activationPromise + + runs -> + spyOn(exMode.mainModule.globalExState, 'setVim').andCallThrough() waitsForPromise -> - vimMode.activate().then -> - helpers.activateExMode().then -> - dir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") - dir2 = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") - fs.makeTreeSync(dir) - fs.makeTreeSync(dir2) - atom.project.setPaths([dir, dir2]) + vimMode.activate() - helpers.getEditorElement (element) -> - atom.commands.dispatch(element, 'ex-mode:open') - keydown('escape') - editorElement = element - editor = editorElement.getModel() - vimState = vimMode.mainModule.getEditorState(editor) - exState = exMode.mainModule.exStates.get(editor) - vimState.activateNormalMode() - vimState.resetNormalMode() - editor.setText("abc\ndef\nabc\ndef") + waitsFor -> + exMode.mainModule.globalExState.setVim.calls.length > 0 + + runs -> + dir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") + dir2 = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") + fs.makeTreeSync(dir) + fs.makeTreeSync(dir2) + atom.project.setPaths([dir, dir2]) + + helpers.getEditorElement (element) -> + atom.commands.dispatch(element, "ex-mode:open") + keydown('escape') + editorElement = element + editor = editorElement.getModel() + vimState = vimMode.mainModule.getEditorState(editor) + exState = exMode.mainModule.exStates.get(editor) + vimState.activateNormalMode() + vimState.resetNormalMode() + editor.setText("abc\ndef\nabc\ndef") afterEach -> fs.removeSync(dir) @@ -253,7 +263,7 @@ describe "the commands", -> submitNormalModeInputText('wq wq-2') expect(Ex.write) .toHaveBeenCalled() - expect(Ex.write.calls[0].args[1].trim()).toEqual('wq-2') + expect(Ex.write.calls[0].args[0].args.trim()).toEqual('wq-2') waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100) describe ":xit", -> From ddbdb861fb994d17d18d1bc4dfc2a04f55c4ef61 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sat, 21 Nov 2015 15:51:42 +0100 Subject: [PATCH 032/131] 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 --- lib/ex.coffee | 106 ++++++++++++++++++++++++++--------- spec/ex-commands-spec.coffee | 97 ++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 27 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 25d3b7e..b5ca23e 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -2,6 +2,7 @@ path = require 'path' CommandError = require './command-error' fs = require 'fs-plus' VimOption = require './vim-option' +_ = require 'underscore-plus' trySave = (func) -> deferred = Promise.defer() @@ -63,6 +64,41 @@ replaceGroups = (groups, string) -> 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 @singleton: => @ex ||= new Ex @@ -196,47 +232,63 @@ class Ex sp: (args) => @split(args) substitute: ({ range, args, editor, vimState }) -> - args = args.trimLeft() - delim = args[0] + args_ = args.trimLeft() + delim = args_[0] if /[a-z1-9\\"|]/i.test(delim) throw new CommandError( "Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") - 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] ?= '' - notDelimRE = new RegExp("\\\\#{delim}", 'g') - spl[0] = spl[0].replace(notDelimRE, delim) - spl[1] = spl[1].replace(notDelimRE, delim) + args_ = args_[1..] + escapeChars = {t: '\t', n: '\n', r: '\r'} + parsed = ['', '', ''] + parsing = 0 + escaped = false + while (char = args_[0])? + args_ = args_[1..] + if char is delim + if not escaped + parsing++ + if parsing > 2 + throw new CommandError('Trailing characters') + else + parsed[parsing] = parsed[parsing][...-1] + else if char is '\\' and not escaped + parsed[parsing] += char + escaped = true + else if parsing == 1 and escaped and escapeChars[char]? + parsed[parsing] += escapeChars[char] + escaped = false + 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 - pattern = new RegExp(spl[0], spl[2]) + flagsObj = {} + flags.split('').forEach((flag) -> flagsObj[flag] = true) + patternRE = getSearchTerm(pattern, flagsObj) 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 - atom.workspace.getActiveTextEditor().transact -> + editor.transact -> 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])) + editor.scanInBufferRange( + patternRE, + [[line, 0], [line + 1, 0]], + ({match, replace}) -> + replace(replaceGroups(match[..], substition)) ) s: (args) => @substitute(args) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 6fd02c3..407b4a9 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -473,6 +473,103 @@ describe "the commands", -> 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", -> beforeEach -> editor.setText('abcaABC\ndefdDEF\nabcaABC') From 4ec1c56077803bb047512fca1d6d9a19dddd145f Mon Sep 17 00:00:00 2001 From: Caio Cutrim Date: Thu, 26 Nov 2015 12:20:32 -0300 Subject: [PATCH 033/131] I added :wa, :qa, :waq shortcuts commands --- lib/ex.coffee | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 684b2f6..e83e7f4 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -71,10 +71,17 @@ class Ex @registerCommand: (name, func) => @singleton()[name] = func + # quit function is used on :q command quit: -> atom.workspace.getActivePane().destroyActiveItem() - + # quit function is used on :qa command shortcut + quitAll: -> + atom.workspace.getPanes()[0].destroy() + # both functions is referenced over here + # when this commands has trigered q: => @quit() + # ...and here + qa: => @quitAll() tabedit: (range, args) => if args.trim() isnt '' @@ -169,17 +176,28 @@ class Ex trySave(-> saveAs(fullPath)).then(deferred.resolve) deferred.promise + # save all files function + saveAll: -> + atom.workspace.saveAll() + # save all then quit + saveAllThenQuit: - > + atom.workspace.saveAll() + @quitAll() w: (args...) => @write(args...) wq: (args...) => @write(args...).then => @quit() + # save all files :wa command + wa: => + @saveAll() + # save all, then quit shortcut + waq: => + @saveAllThenQuit() xit: (args...) => @wq(args...) - wa: -> - atom.workspace.saveAll() split: (range, args) -> args = args.trim() @@ -199,9 +217,9 @@ class Ex substitute: (range, args) -> args = args.trimLeft() delim = args[0] - if /[a-z1-9\\"|]/i.test(delim) + if /[a-z]/i.test(delim) throw new CommandError( - "Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") + "Regular expressions can't be delimited by letters") delimRE = new RegExp("[^\\\\]#{delim}") spl = [] args_ = args[1..] From 10fbdfe969a880ec346d02e56adc8b8498ecca8d Mon Sep 17 00:00:00 2001 From: Caio Cutrim Date: Thu, 26 Nov 2015 17:16:24 -0300 Subject: [PATCH 034/131] fixed saveAllThenQuit function --- lib/ex.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index e83e7f4..34bb1db 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -180,7 +180,7 @@ class Ex saveAll: -> atom.workspace.saveAll() # save all then quit - saveAllThenQuit: - > + saveAllThenQuit: -> atom.workspace.saveAll() @quitAll() From 3c78f9c98540266af5e5b0dc0dae6df020c2538b Mon Sep 17 00:00:00 2001 From: Thomas David Baker Date: Mon, 7 Dec 2015 11:20:57 -0800 Subject: [PATCH 035/131] :saveas command and spec test. --- lib/ex.coffee | 20 +++++-- spec/ex-commands-spec.coffee | 102 +++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 684b2f6..cde0aaa 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -139,7 +139,7 @@ class Ex buffer.setPath(undefined) buffer.load() - write: (range, filePath) -> + write: (range, filePath, saveas = false) -> if filePath[0] is '!' force = true filePath = filePath[1..] @@ -157,16 +157,23 @@ class Ex if filePath.length isnt 0 fullPath = getFullPath(filePath) if editor.getPath()? and (not fullPath? or editor.getPath() == fullPath) - # Use editor.save when no path is given or the path to the file is given - trySave(-> editor.save()).then(deferred.resolve) - saved = true + if saveas + throw new CommandError("Argument required") + else + # Use editor.save when no path is given or the path to the file is given + trySave(-> editor.save()).then(deferred.resolve) + saved = true else if not fullPath? fullPath = atom.showSaveDialogSync() if not saved and fullPath? if not force and fs.existsSync(fullPath) throw new CommandError("File exists (add ! to override)") - trySave(-> saveAs(fullPath)).then(deferred.resolve) + if saveas + editor = atom.workspace.getActiveTextEditor() + trySave(-> editor.saveAs(fullPath)).then(deferred.resolve) + else + trySave(-> saveAs(fullPath)).then(deferred.resolve) deferred.promise @@ -176,6 +183,9 @@ class Ex wq: (args...) => @write(args...).then => @quit() + saveas: (range, filePath) => + @write(range, filePath, true) + xit: (args...) => @wq(args...) wa: -> diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index a4813b5..5421b8e 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -151,6 +151,108 @@ describe "the commands", -> expect(atom.notifications.notifications).toEqual([]) expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef') + describe ":saveas", -> + describe "when editing a new file", -> + beforeEach -> + editor.getBuffer().setText('abc\ndef') + + it "opens the save dialog", -> + spyOn(atom, 'showSaveDialogSync') + keydown(':') + submitNormalModeInputText('saveas') + expect(atom.showSaveDialogSync).toHaveBeenCalled() + + it "saves when a path is specified in the save dialog", -> + filePath = projectPath('saveas-from-save-dialog') + spyOn(atom, 'showSaveDialogSync').andReturn(filePath) + keydown(':') + submitNormalModeInputText('saveas') + expect(fs.existsSync(filePath)).toBe(true) + expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc\ndef') + + it "saves when a path is specified in the save dialog", -> + spyOn(atom, 'showSaveDialogSync').andReturn(undefined) + spyOn(fs, 'writeFileSync') + keydown(':') + submitNormalModeInputText('saveas') + expect(fs.writeFileSync.calls.length).toBe(0) + + describe "when editing an existing file", -> + filePath = '' + i = 0 + + beforeEach -> + i++ + filePath = projectPath("saveas-#{i}") + editor.setText('abc\ndef') + editor.saveAs(filePath) + + it "complains if no path given", -> + editor.setText('abc') + keydown(':') + submitNormalModeInputText('saveas') + expect(atom.notifications.notifications[0].message).toEqual( + 'Command error: Argument required' + ) + + describe "with a specified path", -> + newPath = '' + + beforeEach -> + newPath = path.relative(dir, "#{filePath}.new") + editor.getBuffer().setText('abc') + keydown(':') + + afterEach -> + submitNormalModeInputText("saveas #{newPath}") + newPath = path.resolve(dir, fs.normalize(newPath)) + expect(fs.existsSync(newPath)).toBe(true) + expect(fs.readFileSync(newPath, 'utf-8')).toEqual('abc') + expect(editor.isModified()).toBe(false) + fs.removeSync(newPath) + + it "saves to the path", -> + + it "expands .", -> + newPath = path.join('.', newPath) + + it "expands ..", -> + newPath = path.join('..', newPath) + + it "expands ~", -> + newPath = path.join('~', newPath) + + it "throws an error with more than one path", -> + keydown(':') + submitNormalModeInputText('saveas path1 path2') + expect(atom.notifications.notifications[0].message).toEqual( + 'Command error: Only one file name allowed' + ) + + describe "when the file already exists", -> + existsPath = '' + + beforeEach -> + existsPath = projectPath('saveas-exists') + fs.writeFileSync(existsPath, 'abc') + + afterEach -> + fs.removeSync(existsPath) + + it "throws an error if the file already exists", -> + keydown(':') + submitNormalModeInputText("saveas #{existsPath}") + expect(atom.notifications.notifications[0].message).toEqual( + 'Command error: File exists (add ! to override)' + ) + expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc') + + it "writes if forced with :saveas!", -> + keydown(':') + submitNormalModeInputText("saveas! #{existsPath}") + expect(atom.notifications.notifications).toEqual([]) + expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef') + describe ":quit", -> pane = null beforeEach -> From 299f83983d440ebed7e9ccb71fa0a5d5bbde2c4c Mon Sep 17 00:00:00 2001 From: jazzpi Date: Mon, 28 Dec 2015 12:38:00 +0100 Subject: [PATCH 036/131] Add specs for the input element --- spec/ex-input-spec.coffee | 84 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 spec/ex-input-spec.coffee diff --git a/spec/ex-input-spec.coffee b/spec/ex-input-spec.coffee new file mode 100644 index 0000000..4555c6f --- /dev/null +++ b/spec/ex-input-spec.coffee @@ -0,0 +1,84 @@ +helpers = require './spec-helper' +describe "the input element", -> + [editor, editorElement, vimState, exState] = [] + beforeEach -> + vimMode = atom.packages.loadPackage('vim-mode') + exMode = atom.packages.loadPackage('ex-mode') + waitsForPromise -> + activationPromise = exMode.activate() + helpers.activateExMode() + activationPromise + + runs -> + spyOn(exMode.mainModule.globalExState, 'setVim').andCallThrough() + + waitsForPromise -> + vimMode.activate() + + waitsFor -> + exMode.mainModule.globalExState.setVim.calls.length > 0 + + runs -> + helpers.getEditorElement (element) -> + atom.commands.dispatch(element, "ex-mode:open") + editorElement = element + editor = editorElement.getModel() + atom.commands.dispatch(getCommandEditor(), "core:cancel") + vimState = vimMode.mainModule.getEditorState(editor) + exState = exMode.mainModule.exStates.get(editor) + vimState.activateNormalMode() + vimState.resetNormalMode() + editor.setText("abc\ndef\nabc\ndef") + + afterEach -> + atom.commands.dispatch(getCommandEditor(), "core:cancel") + + getVisibility = () -> + editor.normalModeInputView.panel.visible + + getCommandEditor = () -> + editor.normalModeInputView.editorElement + + it "opens with 'ex-mode:open'", -> + atom.commands.dispatch(editorElement, "ex-mode:open") + expect(getVisibility()).toBe true + + it "closes with 'core:cancel'", -> + atom.commands.dispatch(editorElement, "ex-mode:open") + expect(getVisibility()).toBe true + atom.commands.dispatch(getCommandEditor(), "core:cancel") + expect(getVisibility()).toBe false + + it "closes when opening and then pressing backspace", -> + atom.commands.dispatch(editorElement, "ex-mode:open") + expect(getVisibility()).toBe true + atom.commands.dispatch(getCommandEditor(), "core:backspace") + expect(getVisibility()).toBe false + + it "doesn't close when there is text and pressing backspace", -> + atom.commands.dispatch(editorElement, "ex-mode:open") + expect(getVisibility()).toBe true + commandEditor = getCommandEditor() + model = commandEditor.getModel() + model.setText('abc') + atom.commands.dispatch(commandEditor, "core:backspace") + expect(getVisibility()).toBe true + expect(model.getText()).toBe 'ab' + + it "closes when there is text and pressing backspace multiple times", -> + atom.commands.dispatch(editorElement, "ex-mode:open") + expect(getVisibility()).toBe true + commandEditor = getCommandEditor() + model = commandEditor.getModel() + model.setText('abc') + atom.commands.dispatch(commandEditor, "core:backspace") + expect(getVisibility()).toBe true + expect(model.getText()).toBe 'ab' + atom.commands.dispatch(commandEditor, "core:backspace") + expect(getVisibility()).toBe true + expect(model.getText()).toBe 'a' + atom.commands.dispatch(commandEditor, "core:backspace") + expect(getVisibility()).toBe true + expect(model.getText()).toBe '' + atom.commands.dispatch(commandEditor, "core:backspace") + expect(getVisibility()).toBe false From 701f27130fa5fa92c5e664d6f06919dcf67ad46b Mon Sep 17 00:00:00 2001 From: jazzpi Date: Mon, 28 Dec 2015 12:45:19 +0100 Subject: [PATCH 037/131] Make Ex.registerAlias accessible from the outside --- README.md | 2 ++ lib/ex-mode.coffee | 1 + 2 files changed, 3 insertions(+) diff --git a/README.md b/README.md index abf3f64..bb7b6e0 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ atom.packages.onDidActivatePackage (pack) -> if pack.name == 'ex-mode' Ex = pack.mainModule.provideEx() Ex.registerCommand 'z', -> console.log("Zzzzzz...") + # Register an alias - Now :W acts like :w + Ex.registerAlias 'W', 'w' ``` See `lib/ex.coffee` for some examples commands. Contributions are very welcome! diff --git a/lib/ex-mode.coffee b/lib/ex-mode.coffee index 1a4979d..e3a15b2 100644 --- a/lib/ex-mode.coffee +++ b/lib/ex-mode.coffee @@ -30,6 +30,7 @@ module.exports = ExMode = provideEx: -> registerCommand: Ex.registerCommand.bind(Ex) + registerAlias: Ex.registerAlias.bind(Ex) consumeVim: (vim) -> @vim = vim From 31875cff793b85c1bb63eb17d0e8d3a559f5cc0c Mon Sep 17 00:00:00 2001 From: jazzpi Date: Mon, 28 Dec 2015 13:01:56 +0100 Subject: [PATCH 038/131] Add specs for aliases --- lib/ex.coffee | 2 +- spec/ex-commands-spec.coffee | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index f998b2f..ca0ca0e 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -107,7 +107,7 @@ class Ex @singleton()[name] = func @registerAlias: (alias, name) => - @singleton()[alias] = @singleton()[name] + @singleton()[alias] = (args) => @singleton()[name](args) quit: -> atom.workspace.getActivePane().destroyActiveItem() diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 89420f9..35bfd22 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -4,7 +4,8 @@ os = require 'os' uuid = require 'node-uuid' helpers = require './spec-helper' -Ex = require('../lib/ex').singleton() +ExClass = require('../lib/ex') +Ex = ExClass.singleton() describe "the commands", -> [editor, editorElement, vimState, exState, dir, dir2] = [] @@ -732,3 +733,21 @@ describe "the commands", -> atom.commands.dispatch(editorElement, 'ex-mode:open') submitNormalModeInputText(':set nonumber') expect(atom.config.get('editor.showLineNumbers')).toBe(false) + + describe "aliases", -> + it "calls the aliased function without arguments", -> + ExClass.registerAlias('W', 'w') + spyOn(Ex, 'write') + keydown(':') + submitNormalModeInputText('W') + expect(Ex.write).toHaveBeenCalled() + + it "calls the aliased function with arguments", -> + ExClass.registerAlias('W', 'write') + spyOn(Ex, 'W').andCallThrough() + spyOn(Ex, 'write') + keydown(':') + submitNormalModeInputText('W') + WArgs = Ex.W.calls[0].args[0] + writeArgs = Ex.write.calls[0].args[0] + expect(WArgs).toBe writeArgs From 059719bee45d33a506ba55f0caac6a5441dfabe8 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Mon, 28 Dec 2015 14:04:19 +0100 Subject: [PATCH 039/131] Fix :quitall --- lib/ex.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 1927bb0..7ee11b0 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -113,7 +113,7 @@ class Ex atom.workspace.getActivePane().destroyActiveItem() quitall: -> - atom.workspace.getPanes()[0].destroy() + atom.close() q: => @quit() @@ -234,7 +234,7 @@ class Ex @wall() wqall: => - atom.workspace.saveAll() + @wall() @quitall() wqa: => From e17a6e55338adb35cbdcf9a6a93e9ebfced3109a Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 3 Jan 2016 13:15:14 +0100 Subject: [PATCH 040/131] Add specs for :wall, :quitall and :wqall --- lib/ex.coffee | 2 +- spec/ex-commands-spec.coffee | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 7ee11b0..be1ba80 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -117,7 +117,7 @@ class Ex q: => @quit() - qa: => @quitall() + qall: => @quitall() tabedit: (args) => if args.args.trim() isnt '' diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 35bfd22..b1b0201 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -162,6 +162,13 @@ describe "the commands", -> expect(atom.notifications.notifications).toEqual([]) expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef') + describe ":wall", -> + it "saves all", -> + spyOn(atom.workspace, 'saveAll') + keydown(':') + submitNormalModeInputText('wall') + expect(atom.workspace.saveAll).toHaveBeenCalled() + describe ":saveas", -> describe "when editing a new file", -> beforeEach -> @@ -288,6 +295,13 @@ describe "the commands", -> submitNormalModeInputText('quit') expect(pane.promptToSaveItem).toHaveBeenCalled() + describe ":quitall", -> + it "closes Atom", -> + spyOn(atom, 'close') + keydown(':') + submitNormalModeInputText('quitall') + expect(atom.close).toHaveBeenCalled() + describe ":tabclose", -> it "acts as an alias to :quit", -> spyOn(Ex, 'tabclose').andCallThrough() @@ -376,6 +390,15 @@ describe "the commands", -> submitNormalModeInputText('xit') expect(Ex.wq).toHaveBeenCalled() + describe ":wqall", -> + it "calls :wall, then :quitall", -> + spyOn(Ex, 'wall') + spyOn(Ex, 'quitall') + keydown(':') + submitNormalModeInputText('wqall') + expect(Ex.wall).toHaveBeenCalled() + expect(Ex.quitall).toHaveBeenCalled() + describe ":edit", -> describe "without a file name", -> it "reloads the file from the disk", -> From 86570f76cc7b292c171422f5c1e7da041da5e0e4 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 3 Jan 2016 13:22:36 +0100 Subject: [PATCH 041/131] Remove "looking for new maintainer" disclaimer --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index bb7b6e0..f5ae584 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ ex-mode for Atom's vim-mode -**I now only rarely use Atom and am looking for a new maintainer for this project. Please chime in [here](https://github.com/lloeki/ex-mode/issues/116).** - ## Use Install both [vim-mode](https://github.com/atom/vim-mode) and ex-mode. Type `:` in command mode. Enter `w` or `write`. From 79a4a8986cba3956979db4283ed04d70b9c3eabd Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 3 Jan 2016 13:29:18 +0100 Subject: [PATCH 042/131] Update changelog for 0.8.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aa8d03..6bf7ada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.8.0 +* Don't allow :s delimiters not allowed by vim (@jacwah) +* Backspace over empty `:` now cancels ex-mode (@shamrin) +* Added option to register alias keys in atom init config (@GertjanReynaert) +* Allow `:substitute` to replace empty with an empty string and replacing the last search item (@jazzpi) +* Added `:wall`, `:quitall` and `:wqall` commands (@caiocutrim) +* Added `:saveas` command (@bakert) + ## 0.6.0 * No project/multiple projects paths (uses first one) * Support for :set From ded67a40b5f7447ba9d87086f15df22b122ca8b2 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 3 Jan 2016 13:33:14 +0100 Subject: [PATCH 043/131] Prepare 0.8.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90e6b31..5bca120 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.7.0", + "version": "0.8.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 145446b8de8ba083efa3d1dddabb3b33db328465 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Thu, 18 Feb 2016 18:12:33 +0100 Subject: [PATCH 044/131] Make aliasing more prominent --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f5ae584..58bcc45 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,16 @@ atom.packages.onDidActivatePackage (pack) -> if pack.name == 'ex-mode' Ex = pack.mainModule.provideEx() Ex.registerCommand 'z', -> console.log("Zzzzzz...") - # Register an alias - Now :W acts like :w - Ex.registerAlias 'W', 'w' +``` + +You can also add aliases: + +```coffee +atom.packages.onDidActivatePackage (pack) -> + if pack.name == 'ex-mode' + Ex = pack.mainModule.provideEx() + Ex.registerAlias 'WQ', 'wq' + Ex.registerAlias 'Wq', 'wq' ``` See `lib/ex.coffee` for some examples commands. Contributions are very welcome! From fdd8b36e62b880a197731b3f125dd85ed4299fbb Mon Sep 17 00:00:00 2001 From: Xiaolong Wang Date: Fri, 18 Mar 2016 14:09:14 -0500 Subject: [PATCH 045/131] adding support for splitright and splitbelow --- lib/ex-mode.coffee | 12 ++++++++++++ lib/ex.coffee | 43 +++++++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/lib/ex-mode.coffee b/lib/ex-mode.coffee index e3a15b2..490003e 100644 --- a/lib/ex-mode.coffee +++ b/lib/ex-mode.coffee @@ -35,3 +35,15 @@ module.exports = ExMode = consumeVim: (vim) -> @vim = vim @globalExState.setVim(vim) + + config: + splitbelow: + title: 'Split below' + description: 'when splitting, split from below' + type: 'boolean' + default: 'false' + splitright: + title: 'Split right' + description: 'when splitting, split from right' + type: 'boolean' + default: 'false' diff --git a/lib/ex.coffee b/lib/ex.coffee index be1ba80..1e81c66 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -258,13 +258,23 @@ class Ex 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.splitUp() - for file in filePaths - do -> - atom.workspace.openURIInPane file, newPane + if atom.config.get('ex-mode.splitbelow') + if filePaths? and filePaths.length > 0 + newPane = pane.splitDown() + for file in filePaths + do -> + atom.workspace.openURIInPane file, newPane + else + pane.splitDown(copyActiveItem: true) else - pane.splitUp(copyActiveItem: true) + if filePaths? and filePaths.length > 0 + newPane = pane.splitUp() + for file in filePaths + do -> + atom.workspace.openURIInPane file, newPane + else + pane.splitUp(copyActiveItem: true) + sp: (args) => @split(args) @@ -335,13 +345,22 @@ class Ex 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() - for file in filePaths - do -> - atom.workspace.openURIInPane file, newPane + if atom.config.get('ex-mode.splitright') + if filePaths? and filePaths.length > 0 + newPane = pane.splitRight() + for file in filePaths + do -> + atom.workspace.openURIInPane file, newPane + else + pane.splitRight(copyActiveItem: true) else - pane.splitLeft(copyActiveItem: true) + if filePaths? and filePaths.length > 0 + newPane = pane.splitLeft() + for file in filePaths + do -> + atom.workspace.openURIInPane file, newPane + else + pane.splitLeft(copyActiveItem: true) vsp: (args) => @vsplit(args) From f8396fb4e4a84f1d809c4ad905ab394ebf16865f Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Sat, 23 Apr 2016 21:33:04 -0400 Subject: [PATCH 046/131] Copy to text to clipboard on delete --- lib/ex.coffee | 7 ++++++- spec/ex-commands-spec.coffee | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index be1ba80..5173495 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -347,7 +347,12 @@ class Ex delete: ({ range }) -> range = [[range[0], 0], [range[1] + 1, 0]] - atom.workspace.getActiveTextEditor().buffer.setTextInRange(range, '') + editor = atom.workspace.getActiveTextEditor() + + text = editor.getTextInBufferRange(range) + atom.clipboard.write(text) + + editor.buffer.setTextInRange(range, '') set: ({ range, args }) -> args = args.trim() diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index b1b0201..f350e1d 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -530,6 +530,11 @@ describe "the commands", -> submitNormalModeInputText('delete') expect(editor.getText()).toEqual('abc\ndef\njkl') + it "copies the deleted text", -> + keydown(':') + submitNormalModeInputText('delete') + expect(atom.clipboard.read()).toEqual('ghi\n') + it "deletes the lines in the given range", -> processedOpStack = false exState.onDidProcessOpStack -> processedOpStack = true From 2a5fe2c3828a1346719b096ae0122444960fcc85 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Sun, 24 Apr 2016 14:56:22 -0400 Subject: [PATCH 047/131] Feature/yanking (#138) * Support yanking * Remove unneeded code from yank spec --- lib/ex.coffee | 5 +++++ spec/ex-commands-spec.coffee | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/ex.coffee b/lib/ex.coffee index 5173495..41aaed6 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -354,6 +354,11 @@ class Ex editor.buffer.setTextInRange(range, '') + yank: ({ range }) -> + range = [[range[0], 0], [range[1] + 1, 0]] + txt = atom.workspace.getActiveTextEditor().getTextInBufferRange(range) + atom.clipboard.write(txt); + set: ({ range, args }) -> args = args.trim() if args == "" diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index f350e1d..2fa05c5 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -590,6 +590,21 @@ describe "the commands", -> submitNormalModeInputText(':%substitute/abc/ghi/ig') expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nghiaghi') + describe ":yank", -> + beforeEach -> + editor.setText('abc\ndef\nghi\njkl') + editor.setCursorBufferPosition([2, 0]) + + it "yanks the current line", -> + keydown(':') + submitNormalModeInputText('yank') + expect(atom.clipboard.read()).toEqual('ghi\n') + + it "yanks the lines in the given range", -> + keydown(':') + submitNormalModeInputText('1,2yank') + expect(atom.clipboard.read()).toEqual('abc\ndef\n') + describe "illegal delimiters", -> test = (delim) -> keydown(':') From a6c979e0a410b619f8b9bbdc4211c599480f2bd1 Mon Sep 17 00:00:00 2001 From: Brian Vanderbusch Date: Sun, 24 Apr 2016 14:37:45 -0500 Subject: [PATCH 048/131] changelog for 0.9.0 release --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf7ada..4902657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.9.0 + +* Added support for yank commands, ex `:1,10y` (@posgarou) +* Added contributor guidelines, including a pull request template + +### Fixes + +* delete commands now add text to clipboard, ex `:1,4d` + ## 0.8.0 * Don't allow :s delimiters not allowed by vim (@jacwah) * Backspace over empty `:` now cancels ex-mode (@shamrin) From b763104cb23ce070bd09cf8c815242e87abbff4d Mon Sep 17 00:00:00 2001 From: Xiaolong Wang Date: Fri, 18 Mar 2016 14:09:14 -0500 Subject: [PATCH 049/131] adding support for splitright and splitbelow --- lib/ex-mode.coffee | 12 ++++++++++++ lib/ex.coffee | 43 +++++++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/lib/ex-mode.coffee b/lib/ex-mode.coffee index e3a15b2..490003e 100644 --- a/lib/ex-mode.coffee +++ b/lib/ex-mode.coffee @@ -35,3 +35,15 @@ module.exports = ExMode = consumeVim: (vim) -> @vim = vim @globalExState.setVim(vim) + + config: + splitbelow: + title: 'Split below' + description: 'when splitting, split from below' + type: 'boolean' + default: 'false' + splitright: + title: 'Split right' + description: 'when splitting, split from right' + type: 'boolean' + default: 'false' diff --git a/lib/ex.coffee b/lib/ex.coffee index 41aaed6..c0ae5fd 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -258,13 +258,23 @@ class Ex 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.splitUp() - for file in filePaths - do -> - atom.workspace.openURIInPane file, newPane + if atom.config.get('ex-mode.splitbelow') + if filePaths? and filePaths.length > 0 + newPane = pane.splitDown() + for file in filePaths + do -> + atom.workspace.openURIInPane file, newPane + else + pane.splitDown(copyActiveItem: true) else - pane.splitUp(copyActiveItem: true) + if filePaths? and filePaths.length > 0 + newPane = pane.splitUp() + for file in filePaths + do -> + atom.workspace.openURIInPane file, newPane + else + pane.splitUp(copyActiveItem: true) + sp: (args) => @split(args) @@ -335,13 +345,22 @@ class Ex 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() - for file in filePaths - do -> - atom.workspace.openURIInPane file, newPane + if atom.config.get('ex-mode.splitright') + if filePaths? and filePaths.length > 0 + newPane = pane.splitRight() + for file in filePaths + do -> + atom.workspace.openURIInPane file, newPane + else + pane.splitRight(copyActiveItem: true) else - pane.splitLeft(copyActiveItem: true) + if filePaths? and filePaths.length > 0 + newPane = pane.splitLeft() + for file in filePaths + do -> + atom.workspace.openURIInPane file, newPane + else + pane.splitLeft(copyActiveItem: true) vsp: (args) => @vsplit(args) From 286db320a80fa888d88f6f9591239815d096ea5f Mon Sep 17 00:00:00 2001 From: Xiaolong Wang Date: Sun, 24 Apr 2016 15:39:57 -0500 Subject: [PATCH 050/131] add test --- spec/ex-commands-spec.coffee | 47 ++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 2fa05c5..cb5fc33 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -497,26 +497,43 @@ describe "the commands", -> expect(atom.workspace.open).toHaveBeenCalled() describe ":split", -> - it "splits the current file upwards", -> + it "splits the current file upwards/downward", -> pane = atom.workspace.getActivePane() - spyOn(pane, 'splitUp').andCallThrough() - filePath = projectPath('split') - editor.saveAs(filePath) - keydown(':') - submitNormalModeInputText('split') - expect(pane.splitUp).toHaveBeenCalled() + if atom.config.get('ex-mode.splitbelow') + spyOn(pane, 'splitDown').andCallThrough() + filePath = projectPath('split') + editor.saveAs(filePath) + keydown(':') + submitNormalModeInputText('split') + expect(pane.splitDown).toHaveBeenCalled() + else + spyOn(pane, 'splitUp').andCallThrough() + filePath = projectPath('split') + editor.saveAs(filePath) + keydown(':') + submitNormalModeInputText('split') + expect(pane.splitUp).toHaveBeenCalled() # FIXME: Should test whether the new pane contains a TextEditor # pointing to the same path describe ":vsplit", -> - it "splits the current file to the left", -> - pane = atom.workspace.getActivePane() - spyOn(pane, 'splitLeft').andCallThrough() - filePath = projectPath('vsplit') - editor.saveAs(filePath) - keydown(':') - submitNormalModeInputText('vsplit') - expect(pane.splitLeft).toHaveBeenCalled() + it "splits the current file to the left/right", -> + if atom.config.get('ex-mode.splitright') + pane = atom.workspace.getActivePane() + spyOn(pane, 'splitRight').andCallThrough() + filePath = projectPath('vsplit') + editor.saveAs(filePath) + keydown(':') + submitNormalModeInputText('vsplit') + expect(pane.splitLeft).toHaveBeenCalled() + else + pane = atom.workspace.getActivePane() + spyOn(pane, 'splitLeft').andCallThrough() + filePath = projectPath('vsplit') + editor.saveAs(filePath) + keydown(':') + submitNormalModeInputText('vsplit') + expect(pane.splitLeft).toHaveBeenCalled() # FIXME: Should test whether the new pane contains a TextEditor # pointing to the same path From bca120d98de735a3dcc167899a3cbd4679de5f64 Mon Sep 17 00:00:00 2001 From: Brian Vanderbusch Date: Sat, 30 Apr 2016 13:35:16 -0500 Subject: [PATCH 051/131] added #133 features to changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4902657..334f338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## 0.9.0 -* Added support for yank commands, ex `:1,10y` (@posgarou) +* Added support for yank commands, ex `:1,10y` (@posgarou) * Added contributor guidelines, including a pull request template +* Added ability to control splitting with `splitright`, and `splitbelow` (@dragonxwang) ### Fixes From e6ab4167c9ba925ed1de90a65194d81a402eed5e Mon Sep 17 00:00:00 2001 From: Brian Vanderbusch Date: Wed, 18 May 2016 10:29:22 -0500 Subject: [PATCH 052/131] Contributing guidelines (#140) * Create Pull Request Template * added contributing guidelines - includes a pull request template * cleanup duped PR docs * finish fix of duped PR template * updated contributing guidelines per #140 * comma cleanup, needs tests info update --- .github/CONTRIBUTING.md | 29 +++++++++++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 14 ++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..31132f0 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Ex-Mode Contributing Guidelines + +Current Maintainers: + +- [@jazzpi](https://github.com/jazzpi) +- [@LongLiveCHIEF](https://github.com/LongLiveCHIEF) + +This project is accepting new maintainers. Interested parties should + +1. Open a new issue, titled: `New Maintainer Request` +2. Assign the issue to [@lloeki](https://github.com/lloeki) +3. The last line of your request should `/cc @jazzpi @LongLiveCHIEF` + +## Pull Requests + +- If the PR *fixes* or should result in the closure of any issues, use the `fixes #` or `closes #` syntax to ensure issue will +close when your PR is merged +- All pull-requests that fix a bug or add a new feature *must* have accompanying tests before they will be merged. If you want +to speed up the merge of your PR, please contribute these tests + - *note*: if you submit a PR but are unsure how to write tests, please begin your PR title with `[needs tests]` +- Please use the [pull request template](PULL_REQUEST_TEMPLATE.md) as a guide for submitting your PR. +- Include a `/cc` for @LongLiveCHIEF and @jazzpi the current maintainers + +## Issues + +- Be aware of the responsibilities of `ex-mode` vs `vim-mode` +- If you have identified a bug we would welcome any Pull Requests that either: + - Fix the issue + - Create failing tests to confirm the bug diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ae22d79 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +Fixes # . + +Changes Proposed in this Pull Request: +- +- +- + +I have written tests for: + +- [ ] New features introduced +- [ ] Bugs fixed +- [ ] Neither (I'm just enhancing tests!) + +@jazzpi @LongLiveCHIEF From a405147fc5ca85085169a144b3a9764e7e64a7d7 Mon Sep 17 00:00:00 2001 From: Brian Vanderbusch Date: Wed, 18 May 2016 10:32:50 -0500 Subject: [PATCH 053/131] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5bca120..0f7e102 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.8.0", + "version": "0.9.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From cd4fb6f359081971dfdc1e672f7638986ac809ca Mon Sep 17 00:00:00 2001 From: Brendon Roberto Date: Thu, 16 Jun 2016 14:58:06 -0400 Subject: [PATCH 054/131] Add tests for tabnew command with arguments --- spec/ex-commands-spec.coffee | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index cb5fc33..08237e3 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -496,6 +496,14 @@ describe "the commands", -> submitNormalModeInputText('tabnew') expect(atom.workspace.open).toHaveBeenCalled() + it "opens a new tab for editing when provided an argument", -> + spyOn(Ex, 'tabnew').andCallThrough() + spyOn(Ex, 'tabedit') + keydown(':') + submitNormalModeInputText('tabnew tabnew-test') + expect(Ex.tabedit) + .toHaveBeenCalledWith(Ex.tabnew.calls[0].args...) + describe ":split", -> it "splits the current file upwards/downward", -> pane = atom.workspace.getActivePane() From 4424eec4cc2a2c0542b3fbd000338983cfa4c662 Mon Sep 17 00:00:00 2001 From: Brendon Roberto Date: Thu, 16 Jun 2016 15:05:58 -0400 Subject: [PATCH 055/131] Fix issue with tabnew forwarding args to tabedit --- lib/ex.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index c0ae5fd..c91beb7 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -127,11 +127,11 @@ class Ex tabe: (args) => @tabedit(args) - tabnew: ({ range, args }) => - if args.trim() is '' + tabnew: (args) => + if args.args.trim() is '' atom.workspace.open() else - @tabedit(range, args) + @tabedit(args) tabclose: (args) => @quit(args) From dfa44b5fa2ab287260076d021e3b4974097b1d13 Mon Sep 17 00:00:00 2001 From: Brian Vanderbusch Date: Fri, 17 Jun 2016 18:27:37 -0500 Subject: [PATCH 056/131] Prepare 0.10.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f7e102..05fa7bc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.9.0", + "version": "0.10.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 959ad085914a01effb8e245cff3b6c192f972553 Mon Sep 17 00:00:00 2001 From: Asa Ayers Date: Tue, 2 Aug 2016 15:17:41 -0700 Subject: [PATCH 057/131] Stop using non-standard Promise.defer() Fixes #147 --- lib/ex.coffee | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index c91beb7..21e4add 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -4,8 +4,17 @@ fs = require 'fs-plus' VimOption = require './vim-option' _ = require 'underscore-plus' +defer = () -> + deferred = {} + deferred.promise = new Promise((resolve, reject) -> + deferred.resolve = resolve + deferred.reject = reject + ) + return deferred + + trySave = (func) -> - deferred = Promise.defer() + deferred = defer() try func() @@ -194,7 +203,7 @@ class Ex if filePath.indexOf(' ') isnt -1 throw new CommandError('Only one file name allowed') - deferred = Promise.defer() + deferred = defer() editor = atom.workspace.getActiveTextEditor() saved = false From ccf4c99b2b7f441eacfdb130216670b0726054ba Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 3 Aug 2016 12:21:51 +0200 Subject: [PATCH 058/131] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 334f338..2ff456c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.0 + +* Stop using non-standard Promise.defer (fixes issue with `:w`) (@AsaAyers) + ## 0.9.0 * Added support for yank commands, ex `:1,10y` (@posgarou) From 74451db75f5f050ac468d7f04e670f64d74f25d1 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 3 Aug 2016 12:24:04 +0200 Subject: [PATCH 059/131] Wrong version in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff456c..c8430d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.10.0 +## 0.11.0 * Stop using non-standard Promise.defer (fixes issue with `:w`) (@AsaAyers) From a59a6f9364399af4b813b45ad7a9d30553dab0b0 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 3 Aug 2016 12:24:34 +0200 Subject: [PATCH 060/131] Prepare 0.11.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05fa7bc..9efaf15 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.10.0", + "version": "0.11.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 7e1e03284ae0195864f0cff3a342d886dcfd83ef Mon Sep 17 00:00:00 2001 From: Stuart Quin Date: Wed, 3 Aug 2016 18:41:13 +0100 Subject: [PATCH 061/131] Issue #29 Command and file autocomplete --- lib/autocomplete.coffee | 66 ++++++++++++++++++++++++++++ lib/ex-view-model.coffee | 18 ++++++++ lib/ex.coffee | 3 ++ spec/autocomplete-spec.coffee | 82 +++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 lib/autocomplete.coffee create mode 100644 spec/autocomplete-spec.coffee diff --git a/lib/autocomplete.coffee b/lib/autocomplete.coffee new file mode 100644 index 0000000..d33ffc3 --- /dev/null +++ b/lib/autocomplete.coffee @@ -0,0 +1,66 @@ +fs = require 'fs' +path = require 'path' +Ex = require './ex' + +module.exports = +class AutoComplete + constructor: (commands) -> + @commands = commands + @resetCompletion() + + resetCompletion: () -> + @autoCompleteIndex = 0 + @autoCompleteText = null + @completions = [] + + getAutocomplete: (text) -> + if !@autoCompleteText + @autoCompleteText = text + + parts = @autoCompleteText.split(' ') + cmd = parts[0] + + if parts.length > 1 + filePath = parts.slice(1).join(' ') + return @getCompletion(() => @getFilePathCompletion(cmd, filePath)) + else + return @getCompletion(() => @getCommandCompletion(cmd)) + + filterByPrefix: (commands, prefix) -> + commands.filter((f) => f.startsWith(prefix)) + + getCompletion: (completeFunc) -> + if @completions.length == 0 + @completions = completeFunc() + + if @completions.length + complete = @completions[@autoCompleteIndex % @completions.length] + @autoCompleteIndex++ + + # Only one result so lets return this directory + if complete.endsWith('/') && @completions.length == 1 + @resetCompletion() + + return complete + + getCommandCompletion: (command) -> + if @completions.length == 0 + return @filterByPrefix(@commands, command) + + getFilePathCompletion: (command, filePath) -> + if filePath.endsWith(path.sep) + basePath = path.dirname(filePath + '.') + baseName = '' + else + basePath = path.dirname(filePath) + baseName = path.basename(filePath) + + files = fs.readdirSync(basePath) + + return @filterByPrefix(files, baseName).map((f) => + filePath = path.join(basePath, f) + if fs.lstatSync(filePath).isDirectory() + return command + ' ' + filePath + path.sep + else + return command + ' ' + filePath + ) diff --git a/lib/ex-view-model.coffee b/lib/ex-view-model.coffee index 0c99be9..c8f1202 100644 --- a/lib/ex-view-model.coffee +++ b/lib/ex-view-model.coffee @@ -1,4 +1,6 @@ {ViewModel, Input} = require './view-model' +AutoComplete = require './autocomplete' +Ex = require './ex' module.exports = class ExViewModel extends ViewModel @@ -6,15 +8,31 @@ class ExViewModel extends ViewModel super(@exCommand, class: 'command') @historyIndex = -1 + @view.editorElement.addEventListener('keydown', @tabAutocomplete) atom.commands.add(@view.editorElement, 'core:move-up', @increaseHistoryEx) atom.commands.add(@view.editorElement, 'core:move-down', @decreaseHistoryEx) + @autoComplete = new AutoComplete(Ex.getCommands()) + restoreHistory: (index) -> @view.editorElement.getModel().setText(@history(index).value) history: (index) -> @exState.getExHistoryItem(index) + tabAutocomplete: (event) => + if event.keyCode == 9 + event.stopPropagation() + event.preventDefault() + + completed = @autoComplete.getAutocomplete(@view.editorElement.getModel().getText()) + if completed + @view.editorElement.getModel().setText(completed) + + return false + else + @autoComplete.resetCompletion() + increaseHistoryEx: => if @history(@historyIndex + 1)? @historyIndex += 1 diff --git a/lib/ex.coffee b/lib/ex.coffee index c91beb7..0e46110 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -109,6 +109,9 @@ class Ex @registerAlias: (alias, name) => @singleton()[alias] = (args) => @singleton()[name](args) + @getCommands: () => + Object.keys(@singleton()) + quit: -> atom.workspace.getActivePane().destroyActiveItem() diff --git a/spec/autocomplete-spec.coffee b/spec/autocomplete-spec.coffee new file mode 100644 index 0000000..49b5a1e --- /dev/null +++ b/spec/autocomplete-spec.coffee @@ -0,0 +1,82 @@ +fs = require 'fs-plus' +path = require 'path' +os = require 'os' +uuid = require 'node-uuid' + +helpers = require './spec-helper' +AutoComplete = require '../lib/autocomplete' + +describe "autocomplete functionality", -> + beforeEach -> + @autoComplete = new AutoComplete(['taba', 'tabb', 'tabc']) + @testDir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") + @testFile1 = path.join(@testDir, "atom-ex-testfile-a.txt") + @testFile2 = path.join(@testDir, "atom-ex-testfile-b.txt") + + runs => + fs.makeTreeSync(@testDir) + fs.closeSync(fs.openSync(@testFile1, 'w')); + fs.closeSync(fs.openSync(@testFile2, 'w')); + spyOn(@autoComplete, 'resetCompletion').andCallThrough() + spyOn(@autoComplete, 'getFilePathCompletion').andCallThrough() + spyOn(@autoComplete, 'getCommandCompletion').andCallThrough() + + afterEach -> + fs.removeSync(@testDir) + + describe "autocomplete commands", -> + beforeEach -> + @completed = @autoComplete.getAutocomplete('tab') + + it "returns taba", -> + expect(@completed).toEqual('taba') + + it "calls command function", -> + expect(@autoComplete.getCommandCompletion.callCount).toBe(1) + + describe "autocomplete commands, then autoComplete again", -> + beforeEach -> + @completed = @autoComplete.getAutocomplete('tab') + @completed = @autoComplete.getAutocomplete('tab') + + it "returns tabb", -> + expect(@completed).toEqual('tabb') + + it "calls command function", -> + expect(@autoComplete.getCommandCompletion.callCount).toBe(1) + + describe "autocomplete directory", -> + beforeEach -> + filePath = path.join(os.tmpdir(), 'atom-ex-mode-spec-') + @completed = @autoComplete.getAutocomplete('tabe ' + filePath) + + it "returns testDir", -> + expected = 'tabe ' + @testDir + path.sep + expect(@completed).toEqual(expected) + + it "clears autocomplete", -> + expect(@autoComplete.resetCompletion.callCount).toBe(1) + + describe "autocomplete directory, then autocomplete again", -> + beforeEach -> + filePath = path.join(os.tmpdir(), 'atom-ex-mode-spec-') + @completed = @autoComplete.getAutocomplete('tabe ' + filePath) + @completed = @autoComplete.getAutocomplete(@completed) + + it "returns test file 1", -> + expect(@completed).toEqual('tabe ' + @testFile1) + + it "lists files twice", -> + expect(@autoComplete.getFilePathCompletion.callCount).toBe(2) + + describe "autocomplete full directory, then autocomplete again", -> + beforeEach -> + filePath = path.join(@testDir, 'a') + @completed = @autoComplete.getAutocomplete('tabe ' + filePath) + @completed = @autoComplete.getAutocomplete(@completed) + + it "returns test file 2", -> + expect(@completed).toEqual('tabe ' + @testFile2) + + it "lists files once", -> + expect(@autoComplete.getFilePathCompletion.callCount).toBe(1) From 6f03cd8bc76ac942a56bbdb3e488496b593ecad5 Mon Sep 17 00:00:00 2001 From: Stuart Quin Date: Wed, 3 Aug 2016 21:34:49 +0100 Subject: [PATCH 062/131] Add support for ~ expansion --- lib/autocomplete.coffee | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/autocomplete.coffee b/lib/autocomplete.coffee index d33ffc3..7f8608f 100644 --- a/lib/autocomplete.coffee +++ b/lib/autocomplete.coffee @@ -1,5 +1,6 @@ fs = require 'fs' path = require 'path' +os = require 'os' Ex = require './ex' module.exports = @@ -13,6 +14,12 @@ class AutoComplete @autoCompleteText = null @completions = [] + expandTilde: (filePath) -> + if filePath.charAt(0) == '~' + return os.homedir() + filePath.slice(1) + else + return filePath + getAutocomplete: (text) -> if !@autoCompleteText @autoCompleteText = text @@ -48,6 +55,8 @@ class AutoComplete return @filterByPrefix(@commands, command) getFilePathCompletion: (command, filePath) -> + filePath = @expandTilde(filePath) + if filePath.endsWith(path.sep) basePath = path.dirname(filePath + '.') baseName = '' From 2b9b2f26e5aa2c5da7929737271eb305904cbb76 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 10 Aug 2016 01:29:21 +0200 Subject: [PATCH 063/131] Update editor when saving a new file with `:w` or `:saveas` Fixes #156 --- lib/ex.coffee | 2 +- spec/ex-commands-spec.coffee | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 21e4add..0f7fee9 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -222,7 +222,7 @@ class Ex if not saved and fullPath? if not force and fs.existsSync(fullPath) throw new CommandError("File exists (add ! to override)") - if saveas + if saveas or editor.getFileName() == null editor = atom.workspace.getActiveTextEditor() trySave(-> editor.saveAs(fullPath, editor)).then(deferred.resolve) else diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 08237e3..1b1178e 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -79,6 +79,7 @@ describe "the commands", -> submitNormalModeInputText('write') expect(fs.existsSync(filePath)).toBe(true) expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc\ndef') + expect(editor.isModified()).toBe(false) it "saves when a path is specified in the save dialog", -> spyOn(atom, 'showSaveDialogSync').andReturn(undefined) From 7bec719a6f8ea7b8f93e7178f68d5750beac9fb0 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 10 Aug 2016 02:01:01 +0200 Subject: [PATCH 064/131] Add splitbelow and splitright options to `:set` --- lib/vim-option.coffee | 24 ++++++++++++++++++++++++ spec/ex-commands-spec.coffee | 28 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/vim-option.coffee b/lib/vim-option.coffee index 2ee056c..4d5968c 100644 --- a/lib/vim-option.coffee +++ b/lib/vim-option.coffee @@ -20,4 +20,28 @@ class VimOption nonu: => @nonumber() + splitright: => + atom.config.set("ex-mode.splitright", true) + + spr: => + @splitright() + + nosplitright: => + atom.config.set("ex-mode.splitright", false) + + nospr: => + @nosplitright() + + splitbelow: => + atom.config.set("ex-mode.splitbelow", true) + + sb: => + @splitbelow() + + nosplitbelow: => + atom.config.set("ex-mode.splitbelow", false) + + nosb: => + @nosplitbelow() + module.exports = VimOption diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 08237e3..e8c48f1 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -802,6 +802,34 @@ describe "the commands", -> submitNormalModeInputText(':set nonumber') expect(atom.config.get('editor.showLineNumbers')).toBe(false) + it "sets (no)sp(lit)r(ight)", -> + keydown(':') + submitNormalModeInputText(':set spr') + expect(atom.config.get('ex-mode.splitright')).toBe(true) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set nospr') + expect(atom.config.get('ex-mode.splitright')).toBe(false) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set splitright') + expect(atom.config.get('ex-mode.splitright')).toBe(true) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set nosplitright') + expect(atom.config.get('ex-mode.splitright')).toBe(false) + + it "sets (no)s(plit)b(elow)", -> + keydown(':') + submitNormalModeInputText(':set sb') + expect(atom.config.get('ex-mode.splitbelow')).toBe(true) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set nosb') + expect(atom.config.get('ex-mode.splitbelow')).toBe(false) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set splitbelow') + expect(atom.config.get('ex-mode.splitbelow')).toBe(true) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set nosplitbelow') + expect(atom.config.get('ex-mode.splitbelow')).toBe(false) + describe "aliases", -> it "calls the aliased function without arguments", -> ExClass.registerAlias('W', 'w') From 70a1987cf7fc25abc355173eac87bd25f3e8cf41 Mon Sep 17 00:00:00 2001 From: Stuart Quin Date: Wed, 10 Aug 2016 08:28:33 +0100 Subject: [PATCH 065/131] Sort commands before autocomplete --- lib/autocomplete.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/autocomplete.coffee b/lib/autocomplete.coffee index 7f8608f..8dcb910 100644 --- a/lib/autocomplete.coffee +++ b/lib/autocomplete.coffee @@ -34,7 +34,7 @@ class AutoComplete return @getCompletion(() => @getCommandCompletion(cmd)) filterByPrefix: (commands, prefix) -> - commands.filter((f) => f.startsWith(prefix)) + commands.sort().filter((f) => f.startsWith(prefix)) getCompletion: (completeFunc) -> if @completions.length == 0 @@ -51,8 +51,7 @@ class AutoComplete return complete getCommandCompletion: (command) -> - if @completions.length == 0 - return @filterByPrefix(@commands, command) + return @filterByPrefix(@commands, command) getFilePathCompletion: (command, filePath) -> filePath = @expandTilde(filePath) From d6090058104eb4ac82946f3ec3058b273b191e5b Mon Sep 17 00:00:00 2001 From: Stuart Quin Date: Thu, 11 Aug 2016 12:55:24 +0100 Subject: [PATCH 066/131] Get Ex commands from instance and prototype --- lib/ex.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 0e46110..d8b87b2 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -110,7 +110,9 @@ class Ex @singleton()[alias] = (args) => @singleton()[name](args) @getCommands: () => - Object.keys(@singleton()) + Object.keys(Ex.singleton()).concat(Object.keys(Ex.prototype)).filter((cmd, index, list) -> + list.indexOf(cmd) == index + ) quit: -> atom.workspace.getActivePane().destroyActiveItem() From 3a104fe061c92cf9f4cb643a9a0e5a9d271af075 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Sun, 14 Aug 2016 21:54:02 +0200 Subject: [PATCH 067/131] Prepare 0.12.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9efaf15..c5cd8aa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.11.0", + "version": "0.12.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 1b8f6238c27cc5468febecdc42156d49eab1136c Mon Sep 17 00:00:00 2001 From: jazzpi Date: Mon, 15 Aug 2016 21:13:32 +0200 Subject: [PATCH 068/131] Add basic support for visual marks < and > Only works with range '<,'>. If there are multiple selections, run the command for each one. Closes #31. --- lib/command.coffee | 63 ++++++++++++++++++++++-------------- lib/ex-state.coffee | 9 ++++++ lib/ex-view-model.coffee | 5 ++- spec/ex-commands-spec.coffee | 25 +++++++++++++- spec/ex-input-spec.coffee | 9 ++++++ 5 files changed, 84 insertions(+), 27 deletions(-) diff --git a/lib/command.coffee b/lib/command.coffee index 172cefb..9097c6c 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -5,7 +5,8 @@ CommandError = require './command-error' class Command constructor: (@editor, @exState) -> - @viewModel = new ExViewModel(@) + @selections = @exState.getSelections() + @viewModel = new ExViewModel(@, Object.keys(@selections).length > 0) parseAddr: (str, curPos) -> if str is '.' @@ -97,32 +98,39 @@ class Command curPos = @editor.getCursorBufferPosition() - if addr1? - address1 = @parseAddr(addr1, curPos) + # Special case: run command on selection. This can't be handled by simply + # parsing the mark since vim-mode doesn't set it (and it would be fairly + # useless with multiple selections) + if addr1 is "'<" and addr2 is "'>" + runOverSelections = true else - # If no addr1 is given (,+3), assume it is '.' - address1 = curPos.row - if off1? - address1 += @parseOffset(off1) + runOverSelections = false + if addr1? + address1 = @parseAddr(addr1, curPos) + else + # If no addr1 is given (,+3), assume it is '.' + address1 = curPos.row + if off1? + address1 += @parseOffset(off1) - address1 = 0 if address1 is -1 + address1 = 0 if address1 is -1 - if address1 < 0 or address1 > lastLine - throw new CommandError('Invalid range') + if address1 < 0 or address1 > lastLine + throw new CommandError('Invalid range') - if addr2? - address2 = @parseAddr(addr2, curPos) - if off2? - address2 += @parseOffset(off2) + if addr2? + address2 = @parseAddr(addr2, curPos) + if off2? + address2 += @parseOffset(off2) - if address2 < 0 or address2 > lastLine - throw new CommandError('Invalid range') + if address2 < 0 or address2 > lastLine + throw new CommandError('Invalid range') - if address2 < address1 - throw new CommandError('Backwards range given') + if address2 < address1 + throw new CommandError('Backwards range given') range = [address1, if address2? then address2 else address1] - cl = cl[match?.length..] + cl = cl[match?.length..] # Step 5: Leading blanks are ignored cl = cl.trimLeft() @@ -149,9 +157,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])? - func({ range, args, @vimState, @exState, @editor }) - else + unless (func = Ex.singleton()[command])? # Step 8: Match command against existing commands matching = (name for name, val of Ex.singleton() when \ name.indexOf(command) is 0) @@ -161,9 +167,16 @@ class Command command = matching[0] func = Ex.singleton()[command] - if func? - func({ range, args, @vimState, @exState, @editor }) + + if func? + if runOverSelections + for id, selection of @selections + bufferRange = selection.getBufferRange() + range = [bufferRange.start.row, bufferRange.end.row] + func({ range, args, @vimState, @exState, @editor }) else - throw new CommandError("Not an editor command: #{input.characters}") + func({ range, args, @vimState, @exState, @editor }) + else + throw new CommandError("Not an editor command: #{input.characters}") module.exports = Command diff --git a/lib/ex-state.coffee b/lib/ex-state.coffee index 7c0f37c..89fbeeb 100644 --- a/lib/ex-state.coffee +++ b/lib/ex-state.coffee @@ -60,4 +60,13 @@ class ExState @clearOpStack() @emitter.emit('processed-op-stack') + # Returns all non-empty selections + getSelections: -> + filtered = {} + for id, selection of @editor.getSelections() + unless selection.isEmpty() + filtered[id] = selection + + return filtered + module.exports = ExState diff --git a/lib/ex-view-model.coffee b/lib/ex-view-model.coffee index c8f1202..08dd9f6 100644 --- a/lib/ex-view-model.coffee +++ b/lib/ex-view-model.coffee @@ -4,10 +4,13 @@ Ex = require './ex' module.exports = class ExViewModel extends ViewModel - constructor: (@exCommand) -> + constructor: (@exCommand, withSelection) -> super(@exCommand, class: 'command') @historyIndex = -1 + if withSelection + @view.editorElement.getModel().setText("'<,'>") + @view.editorElement.addEventListener('keydown', @tabAutocomplete) atom.commands.add(@view.editorElement, 'core:move-up', @increaseHistoryEx) atom.commands.add(@view.editorElement, 'core:move-down', @decreaseHistoryEx) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index b58c31a..1dd24a6 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -36,7 +36,8 @@ describe "the commands", -> helpers.getEditorElement (element) -> atom.commands.dispatch(element, "ex-mode:open") - keydown('escape') + atom.commands.dispatch(element.getModel().normalModeInputView.editorElement, + "core:cancel") editorElement = element editor = editorElement.getModel() vimState = vimMode.mainModule.getEditorState(editor) @@ -848,3 +849,25 @@ describe "the commands", -> WArgs = Ex.W.calls[0].args[0] writeArgs = Ex.write.calls[0].args[0] expect(WArgs).toBe writeArgs + + describe "with selections", -> + it "executes on the selected range", -> + spyOn(Ex, 's') + editor.setCursorBufferPosition([0, 0]) + editor.selectToBufferPosition([2, 1]) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText("'<,'>s/abc/def") + expect(Ex.s.calls[0].args[0].range).toEqual [0, 2] + + it "calls the functions multiple times if there are multiple selections", -> + spyOn(Ex, 's') + editor.setCursorBufferPosition([0, 0]) + editor.selectToBufferPosition([2, 1]) + editor.addCursorAtBufferPosition([3, 0]) + editor.selectToBufferPosition([3, 2]) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText("'<,'>s/abc/def") + calls = Ex.s.calls + expect(calls.length).toEqual 2 + expect(calls[0].args[0].range).toEqual [0, 2] + expect(calls[1].args[0].range).toEqual [3, 3] diff --git a/spec/ex-input-spec.coffee b/spec/ex-input-spec.coffee index 4555c6f..3d076b9 100644 --- a/spec/ex-input-spec.coffee +++ b/spec/ex-input-spec.coffee @@ -70,6 +70,7 @@ describe "the input element", -> expect(getVisibility()).toBe true commandEditor = getCommandEditor() model = commandEditor.getModel() + expect(model.getText()).toBe '' model.setText('abc') atom.commands.dispatch(commandEditor, "core:backspace") expect(getVisibility()).toBe true @@ -82,3 +83,11 @@ describe "the input element", -> expect(model.getText()).toBe '' atom.commands.dispatch(commandEditor, "core:backspace") expect(getVisibility()).toBe false + + it "contains '<,'> when opened while there are selections", -> + editor.setCursorBufferPosition([0, 0]) + editor.selectToBufferPosition([0, 1]) + editor.addCursorAtBufferPosition([2, 0]) + editor.selectToBufferPosition([2, 1]) + atom.commands.dispatch(editorElement, "ex-mode:open") + expect(getCommandEditor().getModel().getText()).toBe "'<,'>" From 5f56b62b7ba50a9c5a0f6512cf6097893c73f94a Mon Sep 17 00:00:00 2001 From: jazzpi Date: Mon, 15 Aug 2016 21:21:52 +0200 Subject: [PATCH 069/131] Fix specs Apparently `keydown(':')` was never actually opening the Ex line (and `keydown('escape')` wasn't closing it so it wasn't noticeable), so now we open it directly with `ex-mode:open`. --- spec/ex-commands-spec.coffee | 163 ++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 80 deletions(-) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 1dd24a6..f4c30cf 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -62,6 +62,9 @@ describe "the commands", -> commandEditor.getModel().setText(text) atom.commands.dispatch(commandEditor, "core:confirm") + openEx = -> + atom.commands.dispatch(editorElement, "ex-mode:open") + describe ":write", -> describe "when editing a new file", -> beforeEach -> @@ -69,14 +72,14 @@ describe "the commands", -> it "opens the save dialog", -> spyOn(atom, 'showSaveDialogSync') - keydown(':') + openEx() submitNormalModeInputText('write') expect(atom.showSaveDialogSync).toHaveBeenCalled() it "saves when a path is specified in the save dialog", -> filePath = projectPath('write-from-save-dialog') spyOn(atom, 'showSaveDialogSync').andReturn(filePath) - keydown(':') + openEx() submitNormalModeInputText('write') expect(fs.existsSync(filePath)).toBe(true) expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc\ndef') @@ -85,7 +88,7 @@ describe "the commands", -> it "saves when a path is specified in the save dialog", -> spyOn(atom, 'showSaveDialogSync').andReturn(undefined) spyOn(fs, 'writeFileSync') - keydown(':') + openEx() submitNormalModeInputText('write') expect(fs.writeFileSync.calls.length).toBe(0) @@ -101,7 +104,7 @@ describe "the commands", -> it "saves the file", -> editor.setText('abc') - keydown(':') + openEx() submitNormalModeInputText('write') expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc') expect(editor.isModified()).toBe(false) @@ -112,7 +115,7 @@ describe "the commands", -> beforeEach -> newPath = path.relative(dir, "#{filePath}.new") editor.getBuffer().setText('abc') - keydown(':') + openEx() afterEach -> submitNormalModeInputText("write #{newPath}") @@ -134,7 +137,7 @@ describe "the commands", -> newPath = path.join('~', newPath) it "throws an error with more than one path", -> - keydown(':') + openEx() submitNormalModeInputText('write path1 path2') expect(atom.notifications.notifications[0].message).toEqual( 'Command error: Only one file name allowed' @@ -151,7 +154,7 @@ describe "the commands", -> fs.removeSync(existsPath) it "throws an error if the file already exists", -> - keydown(':') + openEx() submitNormalModeInputText("write #{existsPath}") expect(atom.notifications.notifications[0].message).toEqual( 'Command error: File exists (add ! to override)' @@ -159,7 +162,7 @@ describe "the commands", -> expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc') it "writes if forced with :write!", -> - keydown(':') + openEx() submitNormalModeInputText("write! #{existsPath}") expect(atom.notifications.notifications).toEqual([]) expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef') @@ -167,7 +170,7 @@ describe "the commands", -> describe ":wall", -> it "saves all", -> spyOn(atom.workspace, 'saveAll') - keydown(':') + openEx() submitNormalModeInputText('wall') expect(atom.workspace.saveAll).toHaveBeenCalled() @@ -178,14 +181,14 @@ describe "the commands", -> it "opens the save dialog", -> spyOn(atom, 'showSaveDialogSync') - keydown(':') + openEx() submitNormalModeInputText('saveas') expect(atom.showSaveDialogSync).toHaveBeenCalled() it "saves when a path is specified in the save dialog", -> filePath = projectPath('saveas-from-save-dialog') spyOn(atom, 'showSaveDialogSync').andReturn(filePath) - keydown(':') + openEx() submitNormalModeInputText('saveas') expect(fs.existsSync(filePath)).toBe(true) expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc\ndef') @@ -193,7 +196,7 @@ describe "the commands", -> it "saves when a path is specified in the save dialog", -> spyOn(atom, 'showSaveDialogSync').andReturn(undefined) spyOn(fs, 'writeFileSync') - keydown(':') + openEx() submitNormalModeInputText('saveas') expect(fs.writeFileSync.calls.length).toBe(0) @@ -209,7 +212,7 @@ describe "the commands", -> it "complains if no path given", -> editor.setText('abc') - keydown(':') + openEx() submitNormalModeInputText('saveas') expect(atom.notifications.notifications[0].message).toEqual( 'Command error: Argument required' @@ -221,7 +224,7 @@ describe "the commands", -> beforeEach -> newPath = path.relative(dir, "#{filePath}.new") editor.getBuffer().setText('abc') - keydown(':') + openEx() afterEach -> submitNormalModeInputText("saveas #{newPath}") @@ -243,7 +246,7 @@ describe "the commands", -> newPath = path.join('~', newPath) it "throws an error with more than one path", -> - keydown(':') + openEx() submitNormalModeInputText('saveas path1 path2') expect(atom.notifications.notifications[0].message).toEqual( 'Command error: Only one file name allowed' @@ -260,7 +263,7 @@ describe "the commands", -> fs.removeSync(existsPath) it "throws an error if the file already exists", -> - keydown(':') + openEx() submitNormalModeInputText("saveas #{existsPath}") expect(atom.notifications.notifications[0].message).toEqual( 'Command error: File exists (add ! to override)' @@ -268,7 +271,7 @@ describe "the commands", -> expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc') it "writes if forced with :saveas!", -> - keydown(':') + openEx() submitNormalModeInputText("saveas! #{existsPath}") expect(atom.notifications.notifications).toEqual([]) expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef') @@ -282,7 +285,7 @@ describe "the commands", -> atom.workspace.open() it "closes the active pane item if not modified", -> - keydown(':') + openEx() submitNormalModeInputText('quit') expect(pane.destroyActiveItem).toHaveBeenCalled() expect(pane.getItems().length).toBe(1) @@ -293,14 +296,14 @@ describe "the commands", -> it "opens the prompt to save", -> spyOn(pane, 'promptToSaveItem') - keydown(':') + openEx() submitNormalModeInputText('quit') expect(pane.promptToSaveItem).toHaveBeenCalled() describe ":quitall", -> it "closes Atom", -> spyOn(atom, 'close') - keydown(':') + openEx() submitNormalModeInputText('quitall') expect(atom.close).toHaveBeenCalled() @@ -308,7 +311,7 @@ describe "the commands", -> it "acts as an alias to :quit", -> spyOn(Ex, 'tabclose').andCallThrough() spyOn(Ex, 'quit').andCallThrough() - keydown(':') + openEx() submitNormalModeInputText('tabclose') expect(Ex.quit).toHaveBeenCalledWith(Ex.tabclose.calls[0].args...) @@ -322,13 +325,13 @@ describe "the commands", -> it "switches to the next tab", -> pane.activateItemAtIndex(1) - keydown(':') + openEx() submitNormalModeInputText('tabnext') expect(pane.getActiveItemIndex()).toBe(2) it "wraps around", -> pane.activateItemAtIndex(pane.getItems().length - 1) - keydown(':') + openEx() submitNormalModeInputText('tabnext') expect(pane.getActiveItemIndex()).toBe(0) @@ -342,13 +345,13 @@ describe "the commands", -> it "switches to the previous tab", -> pane.activateItemAtIndex(1) - keydown(':') + openEx() submitNormalModeInputText('tabprevious') expect(pane.getActiveItemIndex()).toBe(0) it "wraps around", -> pane.activateItemAtIndex(0) - keydown(':') + openEx() submitNormalModeInputText('tabprevious') expect(pane.getActiveItemIndex()).toBe(pane.getItems().length - 1) @@ -359,7 +362,7 @@ describe "the commands", -> it "writes the file, then quits", -> spyOn(atom, 'showSaveDialogSync').andReturn(projectPath('wq-1')) - keydown(':') + openEx() submitNormalModeInputText('wq') expect(Ex.write).toHaveBeenCalled() # Since `:wq` only calls `:quit` after `:write` is finished, we need to @@ -368,7 +371,7 @@ describe "the commands", -> it "doesn't quit when the file is new and no path is specified in the save dialog", -> spyOn(atom, 'showSaveDialogSync').andReturn(undefined) - keydown(':') + openEx() submitNormalModeInputText('wq') expect(Ex.write).toHaveBeenCalled() wasNotCalled = false @@ -378,7 +381,7 @@ describe "the commands", -> waitsFor((-> wasNotCalled), 100) it "passes the file name", -> - keydown(':') + openEx() submitNormalModeInputText('wq wq-2') expect(Ex.write) .toHaveBeenCalled() @@ -388,7 +391,7 @@ describe "the commands", -> describe ":xit", -> it "acts as an alias to :wq", -> spyOn(Ex, 'wq') - keydown(':') + openEx() submitNormalModeInputText('xit') expect(Ex.wq).toHaveBeenCalled() @@ -396,7 +399,7 @@ describe "the commands", -> it "calls :wall, then :quitall", -> spyOn(Ex, 'wall') spyOn(Ex, 'quitall') - keydown(':') + openEx() submitNormalModeInputText('wqall') expect(Ex.wall).toHaveBeenCalled() expect(Ex.quitall).toHaveBeenCalled() @@ -408,7 +411,7 @@ describe "the commands", -> editor.getBuffer().setText('abc') editor.saveAs(filePath) fs.writeFileSync(filePath, 'def') - keydown(':') + openEx() submitNormalModeInputText('edit') # Reloading takes a bit waitsFor((-> editor.getText() is 'def'), @@ -420,7 +423,7 @@ describe "the commands", -> editor.saveAs(filePath) editor.getBuffer().setText('abcd') fs.writeFileSync(filePath, 'def') - keydown(':') + openEx() submitNormalModeInputText('edit') expect(atom.notifications.notifications[0].message).toEqual( 'Command error: No write since last change (add ! to override)') @@ -434,7 +437,7 @@ describe "the commands", -> editor.saveAs(filePath) editor.getBuffer().setText('abcd') fs.writeFileSync(filePath, 'def') - keydown(':') + openEx() submitNormalModeInputText('edit!') expect(atom.notifications.notifications.length).toBe(0) waitsFor((-> editor.getText() is 'def') @@ -442,7 +445,7 @@ describe "the commands", -> it "throws an error when editing a new file", -> editor.getBuffer().reload() - keydown(':') + openEx() submitNormalModeInputText('edit') expect(atom.notifications.notifications[0].message).toEqual( 'Command error: No file name') @@ -458,18 +461,18 @@ describe "the commands", -> it "opens the specified path", -> filePath = projectPath('edit-new-test') - keydown(':') + openEx() submitNormalModeInputText("edit #{filePath}") expect(atom.workspace.open).toHaveBeenCalledWith(filePath) it "opens a relative path", -> - keydown(':') + openEx() submitNormalModeInputText('edit edit-relative-test') expect(atom.workspace.open).toHaveBeenCalledWith( projectPath('edit-relative-test')) it "throws an error if trying to open more than one file", -> - keydown(':') + openEx() submitNormalModeInputText('edit edit-new-test-1 edit-new-test-2') expect(atom.workspace.open.callCount).toBe(0) expect(atom.notifications.notifications[0].message).toEqual( @@ -479,14 +482,14 @@ describe "the commands", -> it "acts as an alias to :edit if supplied with a path", -> spyOn(Ex, 'tabedit').andCallThrough() spyOn(Ex, 'edit') - keydown(':') + openEx() submitNormalModeInputText('tabedit tabedit-test') expect(Ex.edit).toHaveBeenCalledWith(Ex.tabedit.calls[0].args...) it "acts as an alias to :tabnew if not supplied with a path", -> spyOn(Ex, 'tabedit').andCallThrough() spyOn(Ex, 'tabnew') - keydown(':') + openEx() submitNormalModeInputText('tabedit ') expect(Ex.tabnew) .toHaveBeenCalledWith(Ex.tabedit.calls[0].args...) @@ -494,14 +497,14 @@ describe "the commands", -> describe ":tabnew", -> it "opens a new tab", -> spyOn(atom.workspace, 'open') - keydown(':') + openEx() submitNormalModeInputText('tabnew') expect(atom.workspace.open).toHaveBeenCalled() it "opens a new tab for editing when provided an argument", -> spyOn(Ex, 'tabnew').andCallThrough() spyOn(Ex, 'tabedit') - keydown(':') + openEx() submitNormalModeInputText('tabnew tabnew-test') expect(Ex.tabedit) .toHaveBeenCalledWith(Ex.tabnew.calls[0].args...) @@ -513,14 +516,14 @@ describe "the commands", -> spyOn(pane, 'splitDown').andCallThrough() filePath = projectPath('split') editor.saveAs(filePath) - keydown(':') + openEx() submitNormalModeInputText('split') expect(pane.splitDown).toHaveBeenCalled() else spyOn(pane, 'splitUp').andCallThrough() filePath = projectPath('split') editor.saveAs(filePath) - keydown(':') + openEx() submitNormalModeInputText('split') expect(pane.splitUp).toHaveBeenCalled() # FIXME: Should test whether the new pane contains a TextEditor @@ -533,7 +536,7 @@ describe "the commands", -> spyOn(pane, 'splitRight').andCallThrough() filePath = projectPath('vsplit') editor.saveAs(filePath) - keydown(':') + openEx() submitNormalModeInputText('vsplit') expect(pane.splitLeft).toHaveBeenCalled() else @@ -541,7 +544,7 @@ describe "the commands", -> spyOn(pane, 'splitLeft').andCallThrough() filePath = projectPath('vsplit') editor.saveAs(filePath) - keydown(':') + openEx() submitNormalModeInputText('vsplit') expect(pane.splitLeft).toHaveBeenCalled() # FIXME: Should test whether the new pane contains a TextEditor @@ -553,19 +556,19 @@ describe "the commands", -> editor.setCursorBufferPosition([2, 0]) it "deletes the current line", -> - keydown(':') + openEx() submitNormalModeInputText('delete') expect(editor.getText()).toEqual('abc\ndef\njkl') it "copies the deleted text", -> - keydown(':') + openEx() submitNormalModeInputText('delete') expect(atom.clipboard.read()).toEqual('ghi\n') it "deletes the lines in the given range", -> processedOpStack = false exState.onDidProcessOpStack -> processedOpStack = true - keydown(':') + openEx() submitNormalModeInputText('1,2delete') expect(editor.getText()).toEqual('ghi\njkl') @@ -578,7 +581,7 @@ describe "the commands", -> expect(editor.getText()).toEqual('abc\n') it "undos deleting several lines at once", -> - keydown(':') + openEx() submitNormalModeInputText('-1,.delete') expect(editor.getText()).toEqual('abc\njkl') atom.commands.dispatch(editorElement, 'core:undo') @@ -590,17 +593,17 @@ describe "the commands", -> editor.setCursorBufferPosition([0, 0]) it "replaces a character on the current line", -> - keydown(':') + openEx() submitNormalModeInputText(':substitute /a/x') expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC') it "doesn't need a space before the arguments", -> - keydown(':') + openEx() submitNormalModeInputText(':substitute/a/x') expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC') it "respects modifiers passed to it", -> - keydown(':') + openEx() submitNormalModeInputText(':substitute/a/x/g') expect(editor.getText()).toEqual('xbcxABC\ndefdDEF\nabcaABC') @@ -609,7 +612,7 @@ describe "the commands", -> expect(editor.getText()).toEqual('xbcxxBC\ndefdDEF\nabcaABC') it "replaces on multiple lines", -> - keydown(':') + openEx() submitNormalModeInputText(':%substitute/abc/ghi') expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nghiaABC') @@ -623,18 +626,18 @@ describe "the commands", -> editor.setCursorBufferPosition([2, 0]) it "yanks the current line", -> - keydown(':') + openEx() submitNormalModeInputText('yank') expect(atom.clipboard.read()).toEqual('ghi\n') it "yanks the lines in the given range", -> - keydown(':') + openEx() submitNormalModeInputText('1,2yank') expect(atom.clipboard.read()).toEqual('abc\ndef\n') describe "illegal delimiters", -> test = (delim) -> - keydown(':') + openEx() submitNormalModeInputText(":substitute #{delim}a#{delim}x#{delim}gi") expect(atom.notifications.notifications[0].message).toEqual( "Command error: Regular expressions can't be delimited by alphanumeric characters, '\\', '\"' or '|'") @@ -651,12 +654,12 @@ describe "the commands", -> editor.setText('abcabc\nabcabc') it "removes the pattern without modifiers", -> - keydown(':') + openEx() submitNormalModeInputText(":substitute/abc//") expect(editor.getText()).toEqual('abc\nabcabc') it "removes the pattern with modifiers", -> - keydown(':') + openEx() submitNormalModeInputText(":substitute/abc//g") expect(editor.getText()).toEqual('\nabcabc') @@ -665,7 +668,7 @@ describe "the commands", -> editor.setText('abc,def,ghi') test = (escapeChar, escaped) -> - keydown(':') + openEx() submitNormalModeInputText(":substitute/,/\\#{escapeChar}/g") expect(editor.getText()).toEqual("abc#{escaped}def#{escaped}ghi") @@ -680,26 +683,26 @@ describe "the commands", -> it "uses case sensitive search if smartcase is off and the pattern is lowercase", -> atom.config.set('vim-mode.useSmartcaseForSearch', false) - keydown(':') + openEx() 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(':') + openEx() 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(':') + openEx() 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(':') + openEx() submitNormalModeInputText(':substitute/ABC/ghi/g') expect(editor.getText()).toEqual('abcaghi\ndefdDEF\nabcaABC') @@ -709,37 +712,37 @@ describe "the commands", -> it "uses case insensitive search if smartcase is off and \c is in the pattern", -> atom.config.set('vim-mode.useSmartcaseForSearch', false) - keydown(':') + openEx() 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(':') + openEx() 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(':') + openEx() 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(':') + openEx() 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(':') + openEx() 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(':') + openEx() submitNormalModeInputText(':substitute/ab\\Cc/ghi/gi') expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nabcaABC') @@ -748,23 +751,23 @@ describe "the commands", -> editor.setText('abcaABC\ndefdDEF\nabcaABC') it "replaces \\1 with the first group", -> - keydown(':') + openEx() submitNormalModeInputText(':substitute/bc(.{2})/X\\1X') expect(editor.getText()).toEqual('aXaAXBC\ndefdDEF\nabcaABC') it "replaces multiple groups", -> - keydown(':') + openEx() submitNormalModeInputText(':substitute/a([a-z]*)aA([A-Z]*)/X\\1XY\\2Y') expect(editor.getText()).toEqual('XbcXYBCY\ndefdDEF\nabcaABC') it "replaces \\0 with the entire match", -> - keydown(':') + openEx() submitNormalModeInputText(':substitute/ab(ca)AB/X\\0X') expect(editor.getText()).toEqual('XabcaABXC\ndefdDEF\nabcaABC') describe ":set", -> it "throws an error without a specified option", -> - keydown(':') + openEx() submitNormalModeInputText(':set') expect(atom.notifications.notifications[0].message).toEqual( 'Command error: No option specified') @@ -772,7 +775,7 @@ describe "the commands", -> it "sets multiple options at once", -> atom.config.set('editor.showInvisibles', false) atom.config.set('editor.showLineNumbers', false) - keydown(':') + openEx() submitNormalModeInputText(':set list number') expect(atom.config.get('editor.showInvisibles')).toBe(true) expect(atom.config.get('editor.showLineNumbers')).toBe(true) @@ -783,7 +786,7 @@ describe "the commands", -> atom.config.set('editor.showLineNumbers', false) it "sets (no)list", -> - keydown(':') + openEx() submitNormalModeInputText(':set list') expect(atom.config.get('editor.showInvisibles')).toBe(true) atom.commands.dispatch(editorElement, 'ex-mode:open') @@ -791,7 +794,7 @@ describe "the commands", -> expect(atom.config.get('editor.showInvisibles')).toBe(false) it "sets (no)nu(mber)", -> - keydown(':') + openEx() submitNormalModeInputText(':set nu') expect(atom.config.get('editor.showLineNumbers')).toBe(true) atom.commands.dispatch(editorElement, 'ex-mode:open') @@ -805,7 +808,7 @@ describe "the commands", -> expect(atom.config.get('editor.showLineNumbers')).toBe(false) it "sets (no)sp(lit)r(ight)", -> - keydown(':') + openEx() submitNormalModeInputText(':set spr') expect(atom.config.get('ex-mode.splitright')).toBe(true) atom.commands.dispatch(editorElement, 'ex-mode:open') @@ -819,7 +822,7 @@ describe "the commands", -> expect(atom.config.get('ex-mode.splitright')).toBe(false) it "sets (no)s(plit)b(elow)", -> - keydown(':') + openEx() submitNormalModeInputText(':set sb') expect(atom.config.get('ex-mode.splitbelow')).toBe(true) atom.commands.dispatch(editorElement, 'ex-mode:open') @@ -836,7 +839,7 @@ describe "the commands", -> it "calls the aliased function without arguments", -> ExClass.registerAlias('W', 'w') spyOn(Ex, 'write') - keydown(':') + openEx() submitNormalModeInputText('W') expect(Ex.write).toHaveBeenCalled() @@ -844,7 +847,7 @@ describe "the commands", -> ExClass.registerAlias('W', 'write') spyOn(Ex, 'W').andCallThrough() spyOn(Ex, 'write') - keydown(':') + openEx() submitNormalModeInputText('W') WArgs = Ex.W.calls[0].args[0] writeArgs = Ex.write.calls[0].args[0] From 773c3c733db6fd5d0b2cbe1ec35f7e3abc4169b3 Mon Sep 17 00:00:00 2001 From: "Jasper v. B" Date: Mon, 15 Aug 2016 23:24:32 +0200 Subject: [PATCH 070/131] Update PR template Remove the @mentions because I'm watching the repository and LongLiveCHIEF no longer maintains it. --- .github/PULL_REQUEST_TEMPLATE.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ae22d79..1b3c90e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,15 @@ Fixes # . Changes Proposed in this Pull Request: -- -- -- + +- foo +- bar +- baz I have written tests for: -- [ ] New features introduced -- [ ] Bugs fixed -- [ ] Neither (I'm just enhancing tests!) +[](Remove the `[]()` to uncomment the appropriate lines) -@jazzpi @LongLiveCHIEF +[](- New features introduced) +[](- Bugs fixed) +[](- Neither (I'm just enhancing tests!)) From a450f5853fef15567ef5f785a353a4fed36726d0 Mon Sep 17 00:00:00 2001 From: "Jasper v. B" Date: Mon, 15 Aug 2016 23:31:08 +0200 Subject: [PATCH 071/131] Update contributing guidelines LongLiveCHIEF no longer maintains this repository and I'm watching it, so the @mentions are unnecessary. Also, only collaborators can assign issues so step #2 doesn't work anyways. --- .github/CONTRIBUTING.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 31132f0..2231100 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,13 +3,8 @@ Current Maintainers: - [@jazzpi](https://github.com/jazzpi) -- [@LongLiveCHIEF](https://github.com/LongLiveCHIEF) -This project is accepting new maintainers. Interested parties should - -1. Open a new issue, titled: `New Maintainer Request` -2. Assign the issue to [@lloeki](https://github.com/lloeki) -3. The last line of your request should `/cc @jazzpi @LongLiveCHIEF` +This project is accepting new maintainers. Interested parties should open a new issue titled `New Maintainer Request`. ## Pull Requests @@ -19,7 +14,6 @@ close when your PR is merged to speed up the merge of your PR, please contribute these tests - *note*: if you submit a PR but are unsure how to write tests, please begin your PR title with `[needs tests]` - Please use the [pull request template](PULL_REQUEST_TEMPLATE.md) as a guide for submitting your PR. -- Include a `/cc` for @LongLiveCHIEF and @jazzpi the current maintainers ## Issues From c6efc0d46c461fe69d32c426844d7892831ae771 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Mon, 15 Aug 2016 23:47:34 +0200 Subject: [PATCH 072/131] Add 'smartcase' option --- lib/vim-option.coffee | 12 ++++++++++++ spec/ex-commands-spec.coffee | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/vim-option.coffee b/lib/vim-option.coffee index 4d5968c..ddf4688 100644 --- a/lib/vim-option.coffee +++ b/lib/vim-option.coffee @@ -44,4 +44,16 @@ class VimOption nosb: => @nosplitbelow() + smartcase: => + atom.config.set("vim-mode.useSmartcaseForSearch", true) + + scs: => + @smartcase() + + nosmartcase: => + atom.config.set("vim-mode.useSmartcaseForSearch", false) + + noscs: => + @nosmartcase() + module.exports = VimOption diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index f4c30cf..72775f8 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -835,6 +835,20 @@ describe "the commands", -> submitNormalModeInputText(':set nosplitbelow') expect(atom.config.get('ex-mode.splitbelow')).toBe(false) + it "sets (no)s(mart)c(a)s(e)", -> + openEx() + submitNormalModeInputText(':set scs') + expect(atom.config.get('vim-mode.useSmartcaseForSearch')).toBe(true) + openEx() + submitNormalModeInputText(':set noscs') + expect(atom.config.get('vim-mode.useSmartcaseForSearch')).toBe(false) + openEx() + submitNormalModeInputText(':set smartcase') + expect(atom.config.get('vim-mode.useSmartcaseForSearch')).toBe(true) + openEx() + submitNormalModeInputText(':set nosmartcase') + expect(atom.config.get('vim-mode.useSmartcaseForSearch')).toBe(false) + describe "aliases", -> it "calls the aliased function without arguments", -> ExClass.registerAlias('W', 'w') From 3c952ccbfe36d0eb72446275b5e576dda5cac308 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Tue, 16 Aug 2016 00:36:05 +0200 Subject: [PATCH 073/131] Fix mark addresses and add specs for addresses Enables properly parsing marks as addresses. Also adds some specs for addresses by checking how ex-mode behaves when used as a motion. Fixes #70. --- lib/command.coffee | 2 +- spec/ex-commands-spec.coffee | 62 ++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/lib/command.coffee b/lib/command.coffee index 9097c6c..ffb9b28 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -24,7 +24,7 @@ class Command mark = @vimState.marks[str[1]] unless mark? throw new CommandError("Mark #{str} not set.") - addr = mark.bufferMarker.range.end.row + addr = mark.getEndBufferPosition().row else if str[0] is "/" addr = Find.findNextInBuffer(@editor.buffer, curPos, str[1...-1]) unless addr? diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 72775f8..4db66ce 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -65,6 +65,68 @@ describe "the commands", -> openEx = -> atom.commands.dispatch(editorElement, "ex-mode:open") + describe "as a motion", -> + beforeEach -> + editor.setCursorBufferPosition([0, 0]) + + it "moves the cursor to a specific line", -> + openEx() + submitNormalModeInputText '2' + + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + + it "moves to the second address", -> + openEx() + submitNormalModeInputText '1,3' + + expect(editor.getCursorBufferPosition()).toEqual [2, 0] + + it "works with offsets", -> + openEx() + submitNormalModeInputText '2+1' + expect(editor.getCursorBufferPosition()).toEqual [2, 0] + + openEx() + submitNormalModeInputText '-2' + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + + it "doesn't move when the address is the current line", -> + openEx() + submitNormalModeInputText '.' + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + + openEx() + submitNormalModeInputText ',' + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + + it "moves to the last line", -> + openEx() + submitNormalModeInputText '$' + expect(editor.getCursorBufferPosition()).toEqual [3, 0] + + it "moves to a mark's line", -> + keydown('l') + keydown('m') + commandModeInputKeydown 'a' + keydown('j') + openEx() + submitNormalModeInputText "'a" + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + + it "moves to a specified search", -> + openEx() + submitNormalModeInputText '/def' + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + + openEx() + submitNormalModeInputText '?abc' + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + + editor.setCursorBufferPosition([3, 0]) + openEx() + submitNormalModeInputText '/ef' + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + describe ":write", -> describe "when editing a new file", -> beforeEach -> From 31ac3a98ed407cbaa5429bd04f72487b0c03c6b2 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Tue, 16 Aug 2016 12:33:00 +0200 Subject: [PATCH 074/131] Fix spec for movement to mark --- spec/ex-commands-spec.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 4db66ce..311f18b 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -107,7 +107,7 @@ describe "the commands", -> it "moves to a mark's line", -> keydown('l') keydown('m') - commandModeInputKeydown 'a' + normalModeInputKeydown 'a' keydown('j') openEx() submitNormalModeInputText "'a" @@ -637,7 +637,6 @@ describe "the commands", -> waitsFor -> processedOpStack editor.setText('abc\ndef\nghi\njkl') editor.setCursorBufferPosition([1, 1]) - # For some reason, keydown(':') doesn't work here :/ atom.commands.dispatch(editorElement, 'ex-mode:open') submitNormalModeInputText(',/k/delete') expect(editor.getText()).toEqual('abc\n') From 0f91ab5ae06f981af4da65e67b53f35d9be44c83 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Tue, 16 Aug 2016 12:55:19 +0200 Subject: [PATCH 075/131] Fix search not working without a closing delimiter --- lib/command.coffee | 33 ++++++++++++-------- lib/find.coffee | 59 ++++++++++++++++++++++++++++++++++++ spec/ex-commands-spec.coffee | 5 +-- 3 files changed, 83 insertions(+), 14 deletions(-) diff --git a/lib/command.coffee b/lib/command.coffee index ffb9b28..48e9616 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -8,14 +8,15 @@ class Command @selections = @exState.getSelections() @viewModel = new ExViewModel(@, Object.keys(@selections).length > 0) - parseAddr: (str, curPos) -> + parseAddr: (str, cursor) -> + row = cursor.getBufferRow() if str is '.' - addr = curPos.row + addr = 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) + addr = row + @parseOffset(str) else if not isNaN(str) addr = parseInt(str) - 1 else if str[0] is "'" # Parse Mark... @@ -26,13 +27,21 @@ class Command throw new CommandError("Mark #{str} not set.") addr = mark.getEndBufferPosition().row else if str[0] is "/" - addr = Find.findNextInBuffer(@editor.buffer, curPos, str[1...-1]) + str = str[1...] + if str[str.length-1] is "/" + str = str[...-1] + addr = Find.scanEditor(str, @editor, cursor.getCurrentLineBufferRange().end)[0] unless addr? - throw new CommandError("Pattern not found: #{str[1...-1]}") + throw new CommandError("Pattern not found: #{str}") + addr = addr.start.row else if str[0] is "?" - addr = Find.findPreviousInBuffer(@editor.buffer, curPos, str[1...-1]) + str = str[1...] + if str[str.length-1] is "?" + str = str[...-1] + addr = Find.scanEditor(str, @editor, cursor.getCurrentLineBufferRange().start, true)[0] unless addr? throw new CommandError("Pattern not found: #{str[1...-1]}") + addr = addr.start.row return addr @@ -76,8 +85,8 @@ class Command \$| # Last line \d+| # n-th line '[\[\]<>'`"^.(){}a-zA-Z]| # Marks - /.*?[^\\]/| # Regex - \?.*?[^\\]\?| # Backwards search + /.*?(?:[^\\]/|$)| # Regex + \?.*?(?:[^\\]\?|$)| # Backwards search [+-]\d* # Current line +/- a number of lines )((?:\s*[+-]\d*)*) # Line offset )? @@ -96,7 +105,7 @@ class Command [match, addr1, off1, addr2, off2] = cl.match(addrPattern) - curPos = @editor.getCursorBufferPosition() + cursor = @editor.getLastCursor() # Special case: run command on selection. This can't be handled by simply # parsing the mark since vim-mode doesn't set it (and it would be fairly @@ -106,10 +115,10 @@ class Command else runOverSelections = false if addr1? - address1 = @parseAddr(addr1, curPos) + address1 = @parseAddr(addr1, cursor) else # If no addr1 is given (,+3), assume it is '.' - address1 = curPos.row + address1 = cursor.getBufferRow() if off1? address1 += @parseOffset(off1) @@ -119,7 +128,7 @@ class Command throw new CommandError('Invalid range') if addr2? - address2 = @parseAddr(addr2, curPos) + address2 = @parseAddr(addr2, cursor) if off2? address2 += @parseOffset(off2) diff --git a/lib/find.coffee b/lib/find.coffee index 53dffc5..60e98b8 100644 --- a/lib/find.coffee +++ b/lib/find.coffee @@ -1,3 +1,40 @@ +_ = require 'underscore-plus' + +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) + module.exports = { findInBuffer : (buffer, pattern) -> found = [] @@ -23,4 +60,26 @@ module.exports = { return found[found.length - 1].start.row else return null + + # Returns an array of ranges of all occurences of `term` in `editor`. + # The array is sorted so that the first occurences after the cursor come + # first (and the search wraps around). If `reverse` is true, the array is + # reversed so that the first occurence before the cursor comes first. + scanEditor: (term, editor, position, reverse = false) -> + [rangesBefore, rangesAfter] = [[], []] + editor.scan getSearchTerm(term), ({range}) -> + if reverse + isBefore = range.start.compare(position) < 0 + else + isBefore = range.start.compare(position) <= 0 + + if isBefore + rangesBefore.push(range) + else + rangesAfter.push(range) + + if reverse + rangesAfter.concat(rangesBefore).reverse() + else + rangesAfter.concat(rangesBefore) } diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 311f18b..bfee143 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -118,9 +118,10 @@ describe "the commands", -> submitNormalModeInputText '/def' expect(editor.getCursorBufferPosition()).toEqual [1, 0] + editor.setCursorBufferPosition([2, 0]) openEx() - submitNormalModeInputText '?abc' - expect(editor.getCursorBufferPosition()).toEqual [0, 0] + submitNormalModeInputText '?def' + expect(editor.getCursorBufferPosition()).toEqual [1, 0] editor.setCursorBufferPosition([3, 0]) openEx() From 17be8f025ae873de85864a5196fa945a670a2cfc Mon Sep 17 00:00:00 2001 From: jazzpi Date: Tue, 16 Aug 2016 13:29:31 +0200 Subject: [PATCH 076/131] Update changelog for v0.12.0 and v0.13.0 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8430d6..78d1819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.13.0 +* Added basic support for visual marks (e.g. `:'<,'>s/foo/bar`) +* Added `smartcase` option to `:set` +* Fixed using marks as addresses (e.g. `:'a,.delete`) +* Fixed search not working without a closing delimiter (e.g. `:/foo`) + +## 0.12.0 +* Added file and command autocomplete (@stuartquin) +* Added `splitbelow` and `splitright` options to `:set` +* Fixed the editor not updating when saving a new file with `:w` or `:saveas` + ## 0.11.0 * Stop using non-standard Promise.defer (fixes issue with `:w`) (@AsaAyers) From 03a36d11cdd5d3d91c99eed3aea0e834f0e2dab6 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Tue, 16 Aug 2016 14:42:03 +0200 Subject: [PATCH 077/131] Prepare 0.13.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5cd8aa..8db31b3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.12.0", + "version": "0.13.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From fd0aa7a6c33f7fa029ff0b631183679cd09bbc0f Mon Sep 17 00:00:00 2001 From: Michael Nicholls Date: Tue, 23 Aug 2016 07:07:44 +0100 Subject: [PATCH 078/131] Add tests for autocompleting a non existent directory Also add tests for autocompleting a file as a directory --- spec/autocomplete-spec.coffee | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/autocomplete-spec.coffee b/spec/autocomplete-spec.coffee index 49b5a1e..000e9c2 100644 --- a/spec/autocomplete-spec.coffee +++ b/spec/autocomplete-spec.coffee @@ -10,6 +10,7 @@ describe "autocomplete functionality", -> beforeEach -> @autoComplete = new AutoComplete(['taba', 'tabb', 'tabc']) @testDir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") + @nonExistentTestDir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}") @testFile1 = path.join(@testDir, "atom-ex-testfile-a.txt") @testFile2 = path.join(@testDir, "atom-ex-testfile-b.txt") @@ -80,3 +81,20 @@ describe "autocomplete functionality", -> it "lists files once", -> expect(@autoComplete.getFilePathCompletion.callCount).toBe(1) + + describe "autocomplete non existent directory", -> + beforeEach -> + @completed = @autoComplete.getAutocomplete('tabe ' + @nonExistentTestDir) + + it "returns no completions", -> + expected = ''; + expect(@completed).toEqual(expected) + + describe "autocomplete existing file as directory", -> + beforeEach -> + filePath = @testFile1 + path.sep + @completed = @autoComplete.getAutocomplete('tabe ' + filePath) + + it "returns no completions", -> + expected = ''; + expect(@completed).toEqual(expected) From 0441b24c21a21bb35b2e7acbf4e7c4392bd3df02 Mon Sep 17 00:00:00 2001 From: Michael Nicholls Date: Tue, 23 Aug 2016 07:11:11 +0100 Subject: [PATCH 079/131] Fix autocompleting a non existent directory --- lib/autocomplete.coffee | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/autocomplete.coffee b/lib/autocomplete.coffee index 8dcb910..6775bac 100644 --- a/lib/autocomplete.coffee +++ b/lib/autocomplete.coffee @@ -40,6 +40,7 @@ class AutoComplete if @completions.length == 0 @completions = completeFunc() + complete = '' if @completions.length complete = @completions[@autoCompleteIndex % @completions.length] @autoCompleteIndex++ @@ -48,7 +49,7 @@ class AutoComplete if complete.endsWith('/') && @completions.length == 1 @resetCompletion() - return complete + return complete getCommandCompletion: (command) -> return @filterByPrefix(@commands, command) @@ -63,12 +64,17 @@ class AutoComplete basePath = path.dirname(filePath) baseName = path.basename(filePath) - files = fs.readdirSync(basePath) - - return @filterByPrefix(files, baseName).map((f) => - filePath = path.join(basePath, f) - if fs.lstatSync(filePath).isDirectory() - return command + ' ' + filePath + path.sep - else - return command + ' ' + filePath - ) + try + basePathStat = fs.statSync(basePath) + if basePathStat.isDirectory() + files = fs.readdirSync(basePath) + return @filterByPrefix(files, baseName).map((f) => + filePath = path.join(basePath, f) + if fs.lstatSync(filePath).isDirectory() + return command + ' ' + filePath + path.sep + else + return command + ' ' + filePath + ) + return [] + catch err + return [] From d5acbd3f53c7ecab7dd43a57a30554bdfc5d6a76 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Fri, 4 Nov 2016 12:51:30 +0100 Subject: [PATCH 080/131] Limit addresses to the last line --- lib/command.coffee | 8 ++++++-- spec/ex-commands-spec.coffee | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/command.coffee b/lib/command.coffee index 48e9616..0cfd6f2 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -123,8 +123,9 @@ class Command address1 += @parseOffset(off1) address1 = 0 if address1 is -1 + address1 = lastLine if address1 > lastLine - if address1 < 0 or address1 > lastLine + if address1 < 0 throw new CommandError('Invalid range') if addr2? @@ -132,7 +133,10 @@ class Command if off2? address2 += @parseOffset(off2) - if address2 < 0 or address2 > lastLine + address2 = 0 if address2 is -1 + address2 = lastLine if address2 > lastLine + + if address2 < 0 throw new CommandError('Invalid range') if address2 < address1 diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index bfee143..2054a38 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -90,6 +90,33 @@ describe "the commands", -> submitNormalModeInputText '-2' expect(editor.getCursorBufferPosition()).toEqual [0, 0] + it "limits to the last line", -> + openEx() + submitNormalModeInputText '10' + expect(editor.getCursorBufferPosition()).toEqual [3, 0] + editor.setCursorBufferPosition([0, 0]) + + openEx() + submitNormalModeInputText '3,10' + expect(editor.getCursorBufferPosition()).toEqual [3, 0] + editor.setCursorBufferPosition([0, 0]) + + openEx() + submitNormalModeInputText '$+1000' + expect(editor.getCursorBufferPosition()).toEqual [3, 0] + editor.setCursorBufferPosition([0, 0]) + + it "goes to the first line with address 0", -> + editor.setCursorBufferPosition([2, 0]) + openEx() + submitNormalModeInputText '0' + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + + editor.setCursorBufferPosition([2, 0]) + openEx() + submitNormalModeInputText '0,0' + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + it "doesn't move when the address is the current line", -> openEx() submitNormalModeInputText '.' From d3c3603eb467a6741babbd55b6113a0683ac5a77 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Fri, 4 Nov 2016 13:07:34 +0100 Subject: [PATCH 081/131] Prepare 0.13.1 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8db31b3..d57a669 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.13.0", + "version": "0.13.1", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From cd80a163cb932c3a8ed59fab1ab1126410dcae2e Mon Sep 17 00:00:00 2001 From: Joey Marianer Date: Sun, 8 Jan 2017 14:29:07 -0800 Subject: [PATCH 082/131] Support :tabonly. Code shamelessly copied from the close-other-tabs extension. --- lib/ex.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/ex.coffee b/lib/ex.coffee index 7c9a6fa..b3d7513 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -163,6 +163,16 @@ class Ex tabp: => @tabprevious() + tabonly: -> + tabBar = atom.workspace.getPanes()[0] + tabBarElement = atom.views.getView(tabBar).querySelector(".tab-bar") + tabBarElement.querySelector(".right-clicked") && tabBarElement.querySelector(".right-clicked").classList.remove("right-clicked") + tabBarElement.querySelector(".active").classList.add("right-clicked") + atom.commands.dispatch(tabBarElement, 'tabs:close-other-tabs') + tabBarElement.querySelector(".active").classList.remove("right-clicked") + + tabo: => @tabonly() + edit: ({ range, args, editor }) -> filePath = args.trim() if filePath[0] is '!' From ba01f8a1b188c6e4757ae521c542120da55a8f52 Mon Sep 17 00:00:00 2001 From: Joey Marianer Date: Sun, 8 Jan 2017 14:27:21 -0800 Subject: [PATCH 083/131] ex-mode supports vim-mode-plus --- README.md | 4 +++- keymaps/ex-mode.cson | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 58bcc45..3f247de 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ ex-mode for Atom's vim-mode ## Use -Install both [vim-mode](https://github.com/atom/vim-mode) and ex-mode. Type `:` in command mode. Enter `w` or `write`. +Install both [vim-mode-plus](https://github.com/t9md/atom-vim-mode-plus) (or +the deprecated `vim-mode`) and ex-mode. Type `:` in command mode. Enter `w` or +`write`. ## Extend diff --git a/keymaps/ex-mode.cson b/keymaps/ex-mode.cson index 5abddd0..1c70132 100644 --- a/keymaps/ex-mode.cson +++ b/keymaps/ex-mode.cson @@ -7,5 +7,7 @@ # For more detailed documentation see # https://atom.io/docs/latest/advanced/keymaps +'atom-text-editor.vim-mode-plus:not(.insert-mode)': + ':': 'ex-mode:open' 'atom-text-editor.vim-mode:not(.insert-mode)': ':': 'ex-mode:open' From ccf6aa22f87a16e9a642f2d7451d3f19d1eb30d8 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 15 Mar 2017 13:55:20 +0100 Subject: [PATCH 084/131] Fix `:x` closing Atom instead of the current pane `x` was being matched with `xall` instead of `xit`, so add an alias. --- lib/ex.coffee | 1 + spec/ex-commands-spec.coffee | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/lib/ex.coffee b/lib/ex.coffee index b3d7513..5edf5a8 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -276,6 +276,7 @@ class Ex xit: (args) => @wq(args) + x: (args) => @xit(args) split: ({ range, args }) -> args = args.trim() diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 2054a38..cc349ac 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -485,6 +485,13 @@ describe "the commands", -> submitNormalModeInputText('xit') expect(Ex.wq).toHaveBeenCalled() + describe ":x", -> + it "acts as an alias to :xit", -> + spyOn(Ex, 'xit') + openEx() + submitNormalModeInputText('x') + expect(Ex.xit).toHaveBeenCalled() + describe ":wqall", -> it "calls :wall, then :quitall", -> spyOn(Ex, 'wall') From cf3e250ea0822eecf021dacd06b6d72e54003646 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 15 Mar 2017 14:59:12 +0100 Subject: [PATCH 085/131] Add changelog for v0.13.1 and v0.14.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78d1819..d7a3578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.14.0 +* Support `:tabonly` (@jmarianer) +* Fix `:x` closing Atom instead of the current pane + +## 0.13.1 +* Limit addresses to the last line +* Fix autocompleting a non existent directory (@mcnicholls) + ## 0.13.0 * Added basic support for visual marks (e.g. `:'<,'>s/foo/bar`) * Added `smartcase` option to `:set` From 28b451f33dead93b1d3ce9474759906f96b23a52 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 15 Mar 2017 15:01:12 +0100 Subject: [PATCH 086/131] Prepare 0.14.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d57a669..7fa69ff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.13.1", + "version": "0.14.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From f4eb1aef7d0607f6c4d12fc0ff91cb648c1a1a0a Mon Sep 17 00:00:00 2001 From: mkiken Date: Mon, 20 Mar 2017 12:03:35 +0900 Subject: [PATCH 087/131] test package.json change. --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 7fa69ff..9f8c99d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,11 @@ }, "consumedServices": { "vim-mode": { + "versions": { + "^0.1.0": "_consumeVim" + } + }, + "vim-mode-plus": { "versions": { "^0.1.0": "consumeVim" } From 50f1beb1e987db5f7c181c8d9c9a3af4b2b7204e Mon Sep 17 00:00:00 2001 From: mkiken Date: Mon, 20 Mar 2017 13:49:33 +0900 Subject: [PATCH 088/131] switch searchHistory --- lib/ex.coffee | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 5edf5a8..d2e9ed9 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -335,12 +335,23 @@ class Ex [pattern, substition, flags] = parsed if pattern is '' - pattern = vimState.getSearchHistoryItem() + if vimState.getSearchHistoryItem? + # vim-mode + pattern = vimState.getSearchHistoryItem() + else if vimState.searchHistory? + #vim-mode-plus + pattern = vimState.searchHistory.get('prev') + if not pattern? atom.beep() throw new CommandError('No previous regular expression') else - vimState.pushSearchHistory(pattern) + if vimState.pushSearchHistory? + # vim-mode + vimState.pushSearchHistory(pattern) + else if vimState.searchHistory? + #vim-mode-plus + vimState.searchHistory.save(pattern) try flagsObj = {} From 0abb61fcb8970658d13fb1fc42d405fbb7fb4748 Mon Sep 17 00:00:00 2001 From: mkiken Date: Mon, 20 Mar 2017 14:02:05 +0900 Subject: [PATCH 089/131] add consumeVimModePlus. --- lib/ex-mode.coffee | 3 +++ package.json | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/ex-mode.coffee b/lib/ex-mode.coffee index 490003e..5274e0a 100644 --- a/lib/ex-mode.coffee +++ b/lib/ex-mode.coffee @@ -36,6 +36,9 @@ module.exports = ExMode = @vim = vim @globalExState.setVim(vim) + consumeVimModePlus: (vim) -> + this.consumeVim(vim) + config: splitbelow: title: 'Split below' diff --git a/package.json b/package.json index 9f8c99d..0984409 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,12 @@ "consumedServices": { "vim-mode": { "versions": { - "^0.1.0": "_consumeVim" + "^0.1.0": "consumeVim" } }, "vim-mode-plus": { "versions": { - "^0.1.0": "consumeVim" + "^0.1.0": "consumeVimModePlus" } } }, From 378cf6cff4e9db8d7af5cb3b2666c97004b08309 Mon Sep 17 00:00:00 2001 From: mkiken Date: Mon, 20 Mar 2017 14:07:44 +0900 Subject: [PATCH 090/131] comment refactoring. --- lib/ex.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index d2e9ed9..e026b96 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -339,7 +339,7 @@ class Ex # vim-mode pattern = vimState.getSearchHistoryItem() else if vimState.searchHistory? - #vim-mode-plus + # vim-mode-plus pattern = vimState.searchHistory.get('prev') if not pattern? @@ -350,7 +350,7 @@ class Ex # vim-mode vimState.pushSearchHistory(pattern) else if vimState.searchHistory? - #vim-mode-plus + # vim-mode-plus vimState.searchHistory.save(pattern) try From 02ab74465c68e9976c57a91bdc26c257e3dc0fd9 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Thu, 25 May 2017 01:08:21 +0200 Subject: [PATCH 091/131] Support vim-mode-plus marks --- lib/command.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/command.coffee b/lib/command.coffee index 0cfd6f2..fcc1144 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -22,7 +22,7 @@ class Command else if str[0] is "'" # Parse Mark... unless @vimState? throw new CommandError("Couldn't get access to vim-mode.") - mark = @vimState.marks[str[1]] + mark = @vimState.mark.marks[str[1]] unless mark? throw new CommandError("Mark #{str} not set.") addr = mark.getEndBufferPosition().row From 708aa94eb0d94b3cb951ab35255739b6428f6dbe Mon Sep 17 00:00:00 2001 From: jazzpi Date: Thu, 25 May 2017 01:08:38 +0200 Subject: [PATCH 092/131] Use vim-mode-plus in specs --- spec/ex-commands-spec.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index cc349ac..4e3d839 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -11,7 +11,7 @@ describe "the commands", -> [editor, editorElement, vimState, exState, dir, dir2] = [] projectPath = (fileName) -> path.join(dir, fileName) beforeEach -> - vimMode = atom.packages.loadPackage('vim-mode') + vimMode = atom.packages.loadPackage('vim-mode-plus') exMode = atom.packages.loadPackage('ex-mode') waitsForPromise -> activationPromise = exMode.activate() @@ -42,7 +42,6 @@ describe "the commands", -> editor = editorElement.getModel() vimState = vimMode.mainModule.getEditorState(editor) exState = exMode.mainModule.exStates.get(editor) - vimState.activateNormalMode() vimState.resetNormalMode() editor.setText("abc\ndef\nabc\ndef") From 99dd953370ded740057119fda1a4761cbd8a4b12 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Thu, 25 May 2017 01:18:58 +0200 Subject: [PATCH 093/131] Use vim-mode-plus on Travis Also use Linux on Travis because there are more machines available --- .travis.yml | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3a85896..e8e09d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,45 @@ -language: objective-c +## Project specific config ### +language: generic + env: - - APM_TEST_PACKAGES="vim-mode" + global: + - APM_TEST_PACKAGES="vim-mode-plus" + - ATOM_LINT_WITH_BUNDLED_NODE="true" + + matrix: + - ATOM_CHANNEL=stable + - ATOM_CHANNEL=beta + +os: + - linux + +dist: trusty + +### Generic setup follows ### +script: + - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh + - chmod u+x build-package.sh + - ./build-package.sh notifications: email: on_success: never on_failure: change -script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' +branches: + only: + - master git: depth: 10 + +sudo: false + +addons: + apt: + packages: + - build-essential + - git + - libgnome-keyring-dev + - libsecret-1-dev + - fakeroot From 546aa9f95c42aaa8b439d64ac85207423e54f710 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Thu, 25 May 2017 01:33:11 +0200 Subject: [PATCH 094/131] Use vim-mode-plus in ex-input-spec as well --- spec/ex-input-spec.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/ex-input-spec.coffee b/spec/ex-input-spec.coffee index 3d076b9..cd7fbd0 100644 --- a/spec/ex-input-spec.coffee +++ b/spec/ex-input-spec.coffee @@ -2,7 +2,7 @@ helpers = require './spec-helper' describe "the input element", -> [editor, editorElement, vimState, exState] = [] beforeEach -> - vimMode = atom.packages.loadPackage('vim-mode') + vimMode = atom.packages.loadPackage('vim-mode-plus') exMode = atom.packages.loadPackage('ex-mode') waitsForPromise -> activationPromise = exMode.activate() @@ -26,7 +26,6 @@ describe "the input element", -> atom.commands.dispatch(getCommandEditor(), "core:cancel") vimState = vimMode.mainModule.getEditorState(editor) exState = exMode.mainModule.exStates.get(editor) - vimState.activateNormalMode() vimState.resetNormalMode() editor.setText("abc\ndef\nabc\ndef") From 9b62ba70ca6fe2d27f7d6f05946a380dcdf80c9c Mon Sep 17 00:00:00 2001 From: jazzpi Date: Thu, 25 May 2017 01:43:49 +0200 Subject: [PATCH 095/131] Add Changelog for v0.15.0 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a3578..d767be1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.15.0 +* `vim-mode-plus` support! + - Add keybinding (@jmarianer) + - Support `:substitute` command (@mkiken) + - Support marks + - Use `vim-mode-plus` in specs + ## 0.14.0 * Support `:tabonly` (@jmarianer) * Fix `:x` closing Atom instead of the current pane From 545f13294e925d5e7b2407aa70f41390eee1c666 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Thu, 25 May 2017 01:45:07 +0200 Subject: [PATCH 096/131] Prepare 0.15.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0984409..8dc63b8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.14.0", + "version": "0.15.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From abb5cd207f025b122522aed40dffe11e08d3d9aa Mon Sep 17 00:00:00 2001 From: Sophie Haskins Date: Thu, 29 Jun 2017 10:32:17 -0400 Subject: [PATCH 097/131] use Atom 1.19 buffer API for finding the length of a buffer --- lib/command.coffee | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/command.coffee b/lib/command.coffee index fcc1144..15e74fe 100644 --- a/lib/command.coffee +++ b/lib/command.coffee @@ -14,7 +14,11 @@ class Command addr = row else if str is '$' # Lines are 0-indexed in Atom, but 1-indexed in vim. - addr = @editor.getBuffer().lines.length - 1 + # The two ways of getting length let us support Atom 1.19's new buffer + # implementation (https://github.com/atom/atom/pull/14435) and still + # support 1.18 and below + buffer = @editor.getBuffer() + addr = (buffer.getLineCount?() ? buffer.lines.length) - 1 else if str[0] in ["+", "-"] addr = row + @parseOffset(str) else if not isNaN(str) @@ -73,7 +77,9 @@ class Command return # Step 4: Address parsing - lastLine = @editor.getBuffer().lines.length - 1 + # see comment in parseAddr about line length + buffer = @editor.getBuffer() + lastLine = (buffer.getLineCount?() ? buffer.lines.length) - 1 if cl[0] is '%' range = [0, lastLine] cl = cl[1..] From dfbbdadfbcd04c3c7a89e10b73c18862b0c7e357 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Thu, 27 Jul 2017 15:14:39 +0200 Subject: [PATCH 098/131] :memo: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d767be1..47ae57c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.16.0 + +* Support for Atom 1.18 and 1.19 (#184) + ## 0.15.0 * `vim-mode-plus` support! - Add keybinding (@jmarianer) From 78a479bf9b8f9fc008d89673dfac00d9a71f273e Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Thu, 27 Jul 2017 15:16:12 +0200 Subject: [PATCH 099/131] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ae57c..d708c94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.16.0 -* Support for Atom 1.18 and 1.19 (#184) +* Support for Atom 1.18 and 1.19 ([#184](https://github.com/lloeki/ex-mode/pull/184)) ## 0.15.0 * `vim-mode-plus` support! From d0e7afe164f9f9b1b2d695972f8aa94ef6b4d4b6 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Thu, 27 Jul 2017 15:17:32 +0200 Subject: [PATCH 100/131] Prepare 0.16.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8dc63b8..26d692b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.15.0", + "version": "0.16.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 3dbcf76ff79cbae59ff30054efa01d919131d414 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Thu, 27 Jul 2017 15:18:20 +0200 Subject: [PATCH 101/131] Prepare 0.17.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 26d692b..1b23c20 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.16.0", + "version": "0.17.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From f2ee5516e03696aa7b3a6bc45c622b51aa13cc7a Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Thu, 27 Jul 2017 15:20:06 +0200 Subject: [PATCH 102/131] Prepare 0.18.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b23c20..cda6de9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.17.0", + "version": "0.18.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From eb7a23717ada8f945c5f42b694b2e05afa2d3e94 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Thu, 27 Jul 2017 15:20:56 +0200 Subject: [PATCH 103/131] Revert back versions, apm publish strugles.. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cda6de9..8dc63b8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.18.0", + "version": "0.15.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 4747bcf5e8dc212f50223799d816fee5406c96bd Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Thu, 27 Jul 2017 15:22:18 +0200 Subject: [PATCH 104/131] :arrow_up: version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8dc63b8..26d692b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.15.0", + "version": "0.16.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From b5cb054b3976288785051b11b2a2ab6365fce32d Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Thu, 27 Jul 2017 15:58:50 +0200 Subject: [PATCH 105/131] Support Promise response in trySave In Atom 1.19, TextBuffer.save returns a Promise. This commit adds support to catch this and resolve our internal callbacks when promise resolves. --- lib/ex.coffee | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index e026b96..4b13d1a 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -17,8 +17,13 @@ trySave = (func) -> deferred = defer() try - func() - deferred.resolve() + response = func() + + if response instanceof Promise + response.then -> + deferred.resolve() + else + deferred.resolve() catch error if error.message.endsWith('is a directory') atom.notifications.addWarning("Unable to save file: #{error.message}") @@ -252,7 +257,7 @@ class Ex @write(args) wq: (args) => - @write(args).then => @quit() + @write(args).then(=> @quit()) wa: => @wall() From 26ac7c50b161ec4d6a7c998f00977de9c6e3a300 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Thu, 27 Jul 2017 16:13:48 +0200 Subject: [PATCH 106/131] Support Ctrl-C to cancel ex-mode --- keymaps/ex-mode.cson | 2 ++ lib/ex-normal-mode-input-element.coffee | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/keymaps/ex-mode.cson b/keymaps/ex-mode.cson index 1c70132..6191513 100644 --- a/keymaps/ex-mode.cson +++ b/keymaps/ex-mode.cson @@ -9,5 +9,7 @@ # https://atom.io/docs/latest/advanced/keymaps 'atom-text-editor.vim-mode-plus:not(.insert-mode)': ':': 'ex-mode:open' +'atom-text-editor.ex-mode-editor': + 'ctrl-c': 'ex-mode:close' 'atom-text-editor.vim-mode:not(.insert-mode)': ':': 'ex-mode:open' diff --git a/lib/ex-normal-mode-input-element.coffee b/lib/ex-normal-mode-input-element.coffee index 73ca3da..915a5be 100644 --- a/lib/ex-normal-mode-input-element.coffee +++ b/lib/ex-normal-mode-input-element.coffee @@ -15,7 +15,8 @@ class ExCommandModeInputElement extends HTMLDivElement @editorContainer.style.height = "0px" @editorElement = document.createElement "atom-text-editor" - @editorElement.classList.add('editor') + @editorElement.classList.add('editor') # Consider this deprecated! + @editorElement.classList.add('ex-mode-editor') @editorElement.getModel().setMini(true) @editorElement.setAttribute('mini', '') @editorContainer.appendChild(@editorElement) @@ -40,6 +41,7 @@ class ExCommandModeInputElement extends HTMLDivElement atom.commands.add(@editorElement, 'core:confirm', @confirm.bind(this)) atom.commands.add(@editorElement, 'core:cancel', @cancel.bind(this)) + atom.commands.add(@editorElement, 'ex-mode:close', @cancel.bind(this)) atom.commands.add(@editorElement, 'blur', @cancel.bind(this)) backspace: -> From 9f1a767fece7b7d64c0708edc6bff9fbfa53f4e3 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Fri, 28 Jul 2017 14:28:03 +0200 Subject: [PATCH 107/131] :memo: update changeling --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d708c94..3dc73c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.17.0 (Unreleased) + +* Added support for canceling ex-mode with Ctrl-C ([#186](https://github.com/lloeki/ex-mode/pull/186)) + ## 0.16.0 * Support for Atom 1.18 and 1.19 ([#184](https://github.com/lloeki/ex-mode/pull/184)) From daddcf8d0fb6baece4be035da601ea3edaedee92 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Sat, 29 Jul 2017 17:38:07 +0200 Subject: [PATCH 108/131] add editorconfig --- .editorconfig | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..33224b4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{coffee,json}] +indent_style = space +indent_size = 2 From 91f748f85f1e4bafc05cf1925df2ad8b1ecc92d7 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Sat, 29 Jul 2017 17:40:59 +0200 Subject: [PATCH 109/131] fix indenting --- lib/ex.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 4b13d1a..2923602 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -20,10 +20,10 @@ trySave = (func) -> response = func() if response instanceof Promise - response.then -> - deferred.resolve() - else + response.then -> deferred.resolve() + else + deferred.resolve() catch error if error.message.endsWith('is a directory') atom.notifications.addWarning("Unable to save file: #{error.message}") From 3283b723942a43eac83cff084bfcbd29b29990ef Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Sat, 29 Jul 2017 17:41:38 +0200 Subject: [PATCH 110/131] :memo: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc73c9..43b94cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.17.0 (Unreleased) +* Add support for Atom 1.19 ([#185](https://github.com/lloeki/ex-mode/pull/185)) * Added support for canceling ex-mode with Ctrl-C ([#186](https://github.com/lloeki/ex-mode/pull/186)) ## 0.16.0 From 8590f5a67877951f29fc2c1da663efdfddb0aef2 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Sat, 29 Jul 2017 17:49:40 +0200 Subject: [PATCH 111/131] :arrow_up: 1.17.0 --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43b94cc..8c0e973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.17.0 (Unreleased) +## 0.17.0 * Add support for Atom 1.19 ([#185](https://github.com/lloeki/ex-mode/pull/185)) * Added support for canceling ex-mode with Ctrl-C ([#186](https://github.com/lloeki/ex-mode/pull/186)) diff --git a/package.json b/package.json index 26d692b..1b23c20 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.16.0", + "version": "0.17.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 117d7439ad5ae59f0192cc0999334998ae0f7728 Mon Sep 17 00:00:00 2001 From: Robby Date: Sun, 6 Aug 2017 21:23:41 -0500 Subject: [PATCH 112/131] Adds save ex-mode command --- lib/ex.coffee | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/ex.coffee b/lib/ex.coffee index e026b96..6913b52 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -133,6 +133,7 @@ class Ex qall: => @quitall() + tabedit: (args) => if args.args.trim() isnt '' @edit(args) @@ -437,4 +438,11 @@ class Ex throw new CommandError("No such option: #{option}") optionProcessor() + sort: ({ range }) => + range = [[range[0], 0], [range[1] + 1, 0]] + editor = atom.workspace.getActiveTextEditor() + text = editor.getTextInBufferRange(range) + sortedText = _.sortBy(text.split(/\n/)).join('\n') + editor.buffer.setTextInRange(range, sortedText) + module.exports = Ex From 15296ff369b87400975ff308487ae05809a31ed8 Mon Sep 17 00:00:00 2001 From: Robby Date: Sun, 6 Aug 2017 21:24:22 -0500 Subject: [PATCH 113/131] Removes accidental newline --- lib/ex.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 6913b52..16f74f9 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -133,7 +133,6 @@ class Ex qall: => @quitall() - tabedit: (args) => if args.args.trim() isnt '' @edit(args) From 43127775084196b1bdbd3038e272a4abaffeefab Mon Sep 17 00:00:00 2001 From: Robby Date: Sun, 6 Aug 2017 23:22:13 -0500 Subject: [PATCH 114/131] Modifies sort function and adds a unit test --- lib/ex.coffee | 17 +++++++++++++---- spec/ex-commands-spec.coffee | 10 ++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 16f74f9..46ab42b 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -438,10 +438,19 @@ class Ex optionProcessor() sort: ({ range }) => - range = [[range[0], 0], [range[1] + 1, 0]] editor = atom.workspace.getActiveTextEditor() - text = editor.getTextInBufferRange(range) - sortedText = _.sortBy(text.split(/\n/)).join('\n') - editor.buffer.setTextInRange(range, sortedText) + sortingRange = [[]] + isMultiLine = range[1] - range[0] > 1 + if isMultiLine + sortingRange = [[range[0], 0], [range[1] + 1, 0]] + else + sortingRange = [[0, 0], [editor.getLastBufferRow(), 0]] + + textLines = [] + for lineIndex in [sortingRange[0][0]..sortingRange[1][0] - 1] + textLines.push(editor.lineTextForBufferRow(lineIndex)) + + sortedText = _.sortBy(textLines).join('\n') + '\n' + editor.buffer.setTextInRange(sortingRange, sortedText) module.exports = Ex diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 4e3d839..2d9facc 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -983,3 +983,13 @@ describe "the commands", -> expect(calls.length).toEqual 2 expect(calls[0].args[0].range).toEqual [0, 2] expect(calls[1].args[0].range).toEqual [3, 3] + + desctibe ':sort', -> + beforeEach -> + editor.setText('ghi\nabc\njkl\ndef\n') + editor.setCursorBufferPosition([0, 0]) + + it "sorts entire file if range is not multi-line", -> + openEx() + submitNormalModeInputText('sort') + expect(atom.getText()).toEqual('abc\ndef\nghi\njkl\n') From d76940dabc05ef7967ecdce5d69cd4b351285c0e Mon Sep 17 00:00:00 2001 From: Robby Date: Sun, 6 Aug 2017 23:40:06 -0500 Subject: [PATCH 115/131] Adds another unit test --- spec/ex-commands-spec.coffee | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 2d9facc..2eb252a 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -984,12 +984,17 @@ describe "the commands", -> expect(calls[0].args[0].range).toEqual [0, 2] expect(calls[1].args[0].range).toEqual [3, 3] - desctibe ':sort', -> + describe ':sort', -> beforeEach -> - editor.setText('ghi\nabc\njkl\ndef\n') + editor.setText('ghi\nabc\njkl\ndef\n142\nzzz\n91xfds9\n') editor.setCursorBufferPosition([0, 0]) it "sorts entire file if range is not multi-line", -> openEx() submitNormalModeInputText('sort') - expect(atom.getText()).toEqual('abc\ndef\nghi\njkl\n') + expect(editor.getText()).toEqual('142\n91xfds9\nabc\ndef\nghi\njkl\nzzz\n') + + it "sorts specific range if range is multi-line", -> + openEx() + submitNormalModeInputText('2,4sort') + expect(editor.getText()).toEqual('ghi\nabc\ndef\njkl\n142\nzzz\n91xfds9\n') From 791c62a3bad988d977a177cac7f76a1adedb0ba5 Mon Sep 17 00:00:00 2001 From: Robert Paul Date: Tue, 8 Aug 2017 14:02:29 -0500 Subject: [PATCH 116/131] Adds clarification comments --- lib/ex.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/ex.coffee b/lib/ex.coffee index 46ab42b..c84fbbf 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -440,16 +440,20 @@ class Ex sort: ({ range }) => editor = atom.workspace.getActiveTextEditor() sortingRange = [[]] + + # If no range is provided, the entire file should be sorted. isMultiLine = range[1] - range[0] > 1 if isMultiLine sortingRange = [[range[0], 0], [range[1] + 1, 0]] else sortingRange = [[0, 0], [editor.getLastBufferRow(), 0]] + # Store every bufferedRow string in an array. textLines = [] for lineIndex in [sortingRange[0][0]..sortingRange[1][0] - 1] textLines.push(editor.lineTextForBufferRow(lineIndex)) + # Sort the array and join them together with newlines for writing back to the file. sortedText = _.sortBy(textLines).join('\n') + '\n' editor.buffer.setTextInRange(sortingRange, sortedText) From 23be6cc862f269d2a0c0cd1d9cd0b6e7d150bfc2 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Tue, 8 Aug 2017 21:12:55 +0200 Subject: [PATCH 117/131] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0e973..bc87c40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## (unpublished) + +* Add :sort command ([#190](https://github.com/lloeki/ex-mode/pull/190)) + ## 0.17.0 * Add support for Atom 1.19 ([#185](https://github.com/lloeki/ex-mode/pull/185)) From 2fa4584eb44d4b0c92952863e2eebdeefa91118f Mon Sep 17 00:00:00 2001 From: mkiken Date: Sun, 13 Aug 2017 16:08:28 +0900 Subject: [PATCH 118/131] add gdefault option --- lib/ex-mode.coffee | 5 +++++ lib/vim-option.coffee | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/lib/ex-mode.coffee b/lib/ex-mode.coffee index 5274e0a..c627483 100644 --- a/lib/ex-mode.coffee +++ b/lib/ex-mode.coffee @@ -50,3 +50,8 @@ module.exports = ExMode = description: 'when splitting, split from right' type: 'boolean' default: 'false' + gdefault: + title: 'Gdefault' + description: 'When on, the ":substitute" flag \'g\' is default on' + type: 'boolean' + default: 'false' diff --git a/lib/vim-option.coffee b/lib/vim-option.coffee index ddf4688..223395f 100644 --- a/lib/vim-option.coffee +++ b/lib/vim-option.coffee @@ -56,4 +56,10 @@ class VimOption noscs: => @nosmartcase() + gdefault: => + atom.config.set("ex-mode.gdefault", true) + + nogdefault: => + atom.config.set("ex-mode.gdefault", false) + module.exports = VimOption From 964813a0b0d5c45bbd93f1a4295071082c3eaf0a Mon Sep 17 00:00:00 2001 From: mkiken Date: Sun, 13 Aug 2017 16:37:00 +0900 Subject: [PATCH 119/131] implement gdefault option --- lib/ex.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index a1e5ad3..cbc7857 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -18,7 +18,7 @@ trySave = (func) -> try response = func() - + if response instanceof Promise response.then -> deferred.resolve() @@ -361,6 +361,9 @@ class Ex try flagsObj = {} flags.split('').forEach((flag) -> flagsObj[flag] = true) + # gdefault option + if atom.config.get('ex-mode.gdefault') + flagsObj.g = !flagsObj.g patternRE = getSearchTerm(pattern, flagsObj) catch e if e.message.indexOf('Invalid flags supplied to RegExp constructor') is 0 From 1a515fcb056db8d6bc09d6f1cf45a040440b9bf2 Mon Sep 17 00:00:00 2001 From: mkiken Date: Sun, 13 Aug 2017 16:47:28 +0900 Subject: [PATCH 120/131] gdefault option set test --- spec/ex-commands-spec.coffee | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 2eb252a..438bbd7 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -944,6 +944,14 @@ describe "the commands", -> submitNormalModeInputText(':set nosmartcase') expect(atom.config.get('vim-mode.useSmartcaseForSearch')).toBe(false) + it "sets (no)gdefault", -> + openEx() + submitNormalModeInputText(':set gdefault') + expect(atom.config.get('ex-mode.gdefault')).toBe(true) + atom.commands.dispatch(editorElement, 'ex-mode:open') + submitNormalModeInputText(':set nogdefault') + expect(atom.config.get('ex-mode.gdefault')).toBe(false) + describe "aliases", -> it "calls the aliased function without arguments", -> ExClass.registerAlias('W', 'w') From d0059a7bb20c7e5716890cec2e76f2c92815f276 Mon Sep 17 00:00:00 2001 From: mkiken Date: Sun, 13 Aug 2017 16:57:11 +0900 Subject: [PATCH 121/131] gdefault option implementation test --- spec/ex-commands-spec.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 438bbd7..49469d0 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -715,6 +715,12 @@ describe "the commands", -> submitNormalModeInputText(':%substitute/abc/ghi/ig') expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nghiaghi') + it "set gdefault option", -> + openEx() + atom.config.set('ex-mode.gdefault', true) + submitNormalModeInputText(':substitute/a/x/g') + expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC') + describe ":yank", -> beforeEach -> editor.setText('abc\ndef\nghi\njkl') From c75395174fa64232e2bc7973b795c17aaf3166e5 Mon Sep 17 00:00:00 2001 From: mkiken Date: Sun, 13 Aug 2017 17:18:26 +0900 Subject: [PATCH 122/131] add gdefault option implementation test --- spec/ex-commands-spec.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/ex-commands-spec.coffee b/spec/ex-commands-spec.coffee index 49469d0..29f211e 100644 --- a/spec/ex-commands-spec.coffee +++ b/spec/ex-commands-spec.coffee @@ -718,6 +718,11 @@ describe "the commands", -> it "set gdefault option", -> openEx() atom.config.set('ex-mode.gdefault', true) + submitNormalModeInputText(':substitute/a/x') + expect(editor.getText()).toEqual('xbcxABC\ndefdDEF\nabcaABC') + + atom.commands.dispatch(editorElement, 'ex-mode:open') + atom.config.set('ex-mode.gdefault', true) submitNormalModeInputText(':substitute/a/x/g') expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC') From 146d832e14ab0b2db75135aa5933df1b47733d4c Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Tue, 15 Aug 2017 21:33:13 +0200 Subject: [PATCH 123/131] Update CHANGELOG.md [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc87c40..c6c943c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## (unpublished) +* Add Gdefault support ([#191](https://github.com/lloeki/ex-mode/pull/191)) * Add :sort command ([#190](https://github.com/lloeki/ex-mode/pull/190)) ## 0.17.0 From 93d0af041fa997ed740b525cb0defd863c37986a Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Sat, 19 Aug 2017 09:43:43 +0200 Subject: [PATCH 124/131] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c943c..41dfadf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## (unpublished) +## 0.18.0 + * Add Gdefault support ([#191](https://github.com/lloeki/ex-mode/pull/191)) * Add :sort command ([#190](https://github.com/lloeki/ex-mode/pull/190)) From 653d62ec1500934750dd8dcd4a528e978fbdee10 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Sat, 19 Aug 2017 09:44:09 +0200 Subject: [PATCH 125/131] Prepare 0.18.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b23c20..cda6de9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ex-mode", "main": "./lib/ex-mode", - "version": "0.17.0", + "version": "0.18.0", "description": "Ex for Atom's vim-mode", "activationCommands": { "atom-workspace": "ex-mode:open" From 4f1ebf8a1a8d5e2e32d099c54656867cd42938af Mon Sep 17 00:00:00 2001 From: Bernard Laveaux Date: Tue, 17 Oct 2017 16:31:54 -0400 Subject: [PATCH 126/131] Support Ctrl-[ to close ex-mode This is merely a suggestion to also default the `ctrl-[` keymap to close ex-mode. This behaviour is very similar to vim's default behaviour: ``` CTRL-[ *c_CTRL-[* *c_* *c_Esc* When typed and 'x' not present in 'cpoptions', quit Command-line mode without executing. In macros or when 'x' present in 'cpoptions', start entered command. Note: If your key is hard to hit on your keyboard, train yourself to use CTRL-[. ``` Is very similar to the currently supported `ctrl-c` ``` CTRL-C *c_CTRL-C* quit command-line without executing ``` --- keymaps/ex-mode.cson | 1 + 1 file changed, 1 insertion(+) diff --git a/keymaps/ex-mode.cson b/keymaps/ex-mode.cson index 6191513..a071efd 100644 --- a/keymaps/ex-mode.cson +++ b/keymaps/ex-mode.cson @@ -11,5 +11,6 @@ ':': 'ex-mode:open' 'atom-text-editor.ex-mode-editor': 'ctrl-c': 'ex-mode:close' + 'ctrl-[': 'ex-mode:close' 'atom-text-editor.vim-mode:not(.insert-mode)': ':': 'ex-mode:open' From 15038d7b0cf9cea653d5ed1a04eb2cfe23106dd6 Mon Sep 17 00:00:00 2001 From: Adrian Wilkins Date: Mon, 26 Mar 2018 13:24:05 +0100 Subject: [PATCH 127/131] Only close buffers on quitall --- lib/ex-mode.coffee | 5 +++++ lib/ex.coffee | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/ex-mode.coffee b/lib/ex-mode.coffee index c627483..d8fc766 100644 --- a/lib/ex-mode.coffee +++ b/lib/ex-mode.coffee @@ -55,3 +55,8 @@ module.exports = ExMode = description: 'When on, the ":substitute" flag \'g\' is default on' type: 'boolean' default: 'false' + onlyCloseBuffers: + title: 'Only close buffers' + description: 'When on, quitall only closes all buffers, not entire Atom instance' + type: 'boolean' + default: 'false' diff --git a/lib/ex.coffee b/lib/ex.coffee index cbc7857..a266afb 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -132,7 +132,11 @@ class Ex atom.workspace.getActivePane().destroyActiveItem() quitall: -> - atom.close() + if !atom.config.get('ex-mode.onlyCloseBuffers') + atom.close() + else + atom.workspace.getTextEditors().forEach (editor) -> + editor.destroy() q: => @quit() From a33959f82996617f10afac096120d842d04035f2 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Tue, 29 May 2018 23:09:45 +0200 Subject: [PATCH 128/131] Fix :enew not working Fixes #214 --- lib/ex.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index a266afb..58b0ebb 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -3,6 +3,7 @@ CommandError = require './command-error' fs = require 'fs-plus' VimOption = require './vim-option' _ = require 'underscore-plus' +atom defer = () -> deferred = {} @@ -210,9 +211,7 @@ class Ex e: (args) => @edit(args) enew: -> - buffer = atom.workspace.getActiveTextEditor().buffer - buffer.setPath(undefined) - buffer.load() + atom.workspace.open() write: ({ range, args, editor, saveas }) -> saveas ?= false From 23dde8c7ee9b778d75deaf33ac0fe3449b68f8b7 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Tue, 29 May 2018 23:14:45 +0200 Subject: [PATCH 129/131] :memo: update CHANGELOG [ci skip] --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41dfadf..30a0532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## (unpublished) +* Fix `:enew` not working ([#215](https://github.com/lloeki/ex-mode/pull/215)) + ## 0.18.0 * Add Gdefault support ([#191](https://github.com/lloeki/ex-mode/pull/191)) From 744ec7351e5a27991d44db38918210d32c50d964 Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Tue, 29 May 2018 23:55:46 +0200 Subject: [PATCH 130/131] Refactor :write command Update the logic to simplify it. Also fix #208 --- lib/ex.coffee | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/ex.coffee b/lib/ex.coffee index 58b0ebb..b262023 100644 --- a/lib/ex.coffee +++ b/lib/ex.coffee @@ -229,29 +229,30 @@ class Ex deferred = defer() editor = atom.workspace.getActiveTextEditor() - saved = false + + # Case 1; path is provided if filePath.length isnt 0 - fullPath = getFullPath(filePath) - if editor.getPath()? and (not fullPath? or editor.getPath() == fullPath) - if saveas - throw new CommandError("Argument required") - else - # Use editor.save when no path is given or the path to the file is given - trySave(-> editor.save()).then(deferred.resolve) - saved = true - else if not fullPath? - fullPath = atom.showSaveDialogSync() + fullPath = getFullPath filePath - if not saved and fullPath? - if not force and fs.existsSync(fullPath) - throw new CommandError("File exists (add ! to override)") - if saveas or editor.getFileName() == null - editor = atom.workspace.getActiveTextEditor() - trySave(-> editor.saveAs(fullPath, editor)).then(deferred.resolve) - else - trySave(-> saveAs(fullPath, editor)).then(deferred.resolve) + # Only write when it does not exist or we have a force flag set. + if force or not fs.existsSync(fullPath) + editor.saveAs(fullPath) + return deferred.promise - deferred.promise + throw new CommandError("File exists (add ! to override)") + + # Case 2; no path provided, call editor save. + editor = atom.workspace.getActiveTextEditor() + + # Does the current buffer exist? + if editor.getPath()? and fs.existsSync(editor.getPath()) + trySave(-> editor.save()).then(deferred.promise) + else + # Cant see what the better API is but Pane.saveActiveItemAs() is the only call + # I could find that states it will ask the user. + trySave(-> atom.workspace.getActivePane().saveActiveItemAs()).then(deferred.promise) + + return deferred.promise wall: -> atom.workspace.saveAll() From 59cafd6b994467c91ed7004a40b3cd743f472474 Mon Sep 17 00:00:00 2001 From: Sarang Joshi Date: Thu, 13 Dec 2018 14:39:51 -0800 Subject: [PATCH 131/131] Add existing command information to README --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) mode change 100644 => 100755 README.md diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 3f247de..ea4a068 --- a/README.md +++ b/README.md @@ -30,7 +30,34 @@ atom.packages.onDidActivatePackage (pack) -> Ex.registerAlias 'Wq', 'wq' ``` -See `lib/ex.coffee` for some examples commands. Contributions are very welcome! +## Existing commands + +This is the baseline list of commands supported in `ex-mode`. + +| Command | Operation | +| --------------------------------------- | ---------------------------------- | +| `q/quit/tabc/tabclose` | Close active tab | +| `qall/quitall` | Close all tabs | +| `tabe/tabedit/tabnew` | Open new tab | +| `e/edit/tabe/tabedit/tabnew ` | Edit given file | +| `tabn/tabnext` | Go to next tab | +| `tabp/tabprevious` | Go to previous tab | +| `tabo/tabonly` | Close other tabs | +| `w/write` | Save active tab | +| `w/write/saveas ` | Save as | +| `wall/wa` | Save all tabs | +| `sp/split` | Split window | +| `sp/split ` | Open file in split window | +| `s/substitute` | Substitute regular expression in active line | +| `vsp/vsplit` | Vertical split window | +| `vsp/vsplit ` | Open file in vertical split window | +| `delete` | Cut active line | +| `yank` | Copy active line | +| `set ` | Set options | +| `sort` | Sort all lines in file | +| `sort ` | Sort lines in line range | + +See `lib/ex.coffee` for the implementations of these commands. Contributions are very welcome! ## Status