From 42a44ee9e1fe92c8a02fb592c90df3d4afaf6769 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 29 Jul 2015 19:13:13 +0200 Subject: [PATCH] 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}