4 uuid = require 'node-uuid'
5 helpers = require './spec-helper'
7 Ex = require('../lib/ex').singleton()
9 describe "the commands", ->
10 [editor, editorElement, vimState, exState, dir, dir2] = []
11 projectPath = (fileName) -> path.join(dir, fileName)
13 vimMode = atom.packages.loadPackage('vim-mode')
14 exMode = atom.packages.loadPackage('ex-mode')
18 vimMode.activate().then ->
19 helpers.activateExMode().then ->
20 dir = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}")
21 dir2 = path.join(os.tmpdir(), "atom-ex-mode-spec-#{uuid.v4()}")
24 atom.project.setPaths([dir, dir2])
26 helpers.getEditorElement (element) ->
27 atom.commands.dispatch(element, 'ex-mode:open')
29 editorElement = element
30 editor = editorElement.getModel()
31 vimState = vimMode.mainModule.getEditorState(editor)
32 exState = exMode.mainModule.exStates.get(editor)
33 vimState.activateNormalMode()
34 vimState.resetNormalMode()
35 editor.setText("abc\ndef\nabc\ndef")
41 keydown = (key, options={}) ->
42 options.element ?= editorElement
43 helpers.keydown(key, options)
45 normalModeInputKeydown = (key, opts = {}) ->
46 editor.normalModeInputView.editorElement.getModel().setText(key)
48 submitNormalModeInputText = (text) ->
49 commandEditor = editor.normalModeInputView.editorElement
50 commandEditor.getModel().setText(text)
51 atom.commands.dispatch(commandEditor, "core:confirm")
54 describe "when editing a new file", ->
56 editor.getBuffer().setText('abc\ndef')
58 it "opens the save dialog", ->
59 spyOn(atom, 'showSaveDialogSync')
61 submitNormalModeInputText('write')
62 expect(atom.showSaveDialogSync).toHaveBeenCalled()
64 it "saves when a path is specified in the save dialog", ->
65 filePath = projectPath('write-from-save-dialog')
66 spyOn(atom, 'showSaveDialogSync').andReturn(filePath)
68 submitNormalModeInputText('write')
69 expect(fs.existsSync(filePath)).toBe(true)
70 expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc\ndef')
72 it "saves when a path is specified in the save dialog", ->
73 spyOn(atom, 'showSaveDialogSync').andReturn(undefined)
74 spyOn(fs, 'writeFileSync')
76 submitNormalModeInputText('write')
77 expect(fs.writeFileSync.calls.length).toBe(0)
79 describe "when editing an existing file", ->
85 filePath = projectPath("write-#{i}")
86 editor.setText('abc\ndef')
87 editor.saveAs(filePath)
89 it "saves the file", ->
92 submitNormalModeInputText('write')
93 expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc')
94 expect(editor.isModified()).toBe(false)
96 describe "with a specified path", ->
100 newPath = path.relative(dir, "#{filePath}.new")
101 editor.getBuffer().setText('abc')
105 submitNormalModeInputText("write #{newPath}")
106 newPath = path.resolve(dir, fs.normalize(newPath))
107 expect(fs.existsSync(newPath)).toBe(true)
108 expect(fs.readFileSync(newPath, 'utf-8')).toEqual('abc')
109 expect(editor.isModified()).toBe(true)
110 fs.removeSync(newPath)
112 it "saves to the path", ->
115 newPath = path.join('.', newPath)
118 newPath = path.join('..', newPath)
121 newPath = path.join('~', newPath)
123 it "throws an error with more than one path", ->
125 submitNormalModeInputText('write path1 path2')
126 expect(atom.notifications.notifications[0].message).toEqual(
127 'Command error: Only one file name allowed'
130 describe "when the file already exists", ->
134 existsPath = projectPath('write-exists')
135 fs.writeFileSync(existsPath, 'abc')
138 fs.removeSync(existsPath)
140 it "throws an error if the file already exists", ->
142 submitNormalModeInputText("write #{existsPath}")
143 expect(atom.notifications.notifications[0].message).toEqual(
144 'Command error: File exists (add ! to override)'
146 expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc')
148 it "writes if forced with :write!", ->
150 submitNormalModeInputText("write! #{existsPath}")
151 expect(atom.notifications.notifications).toEqual([])
152 expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef')
158 pane = atom.workspace.getActivePane()
159 spyOn(pane, 'destroyActiveItem').andCallThrough()
160 atom.workspace.open()
162 it "closes the active pane item if not modified", ->
164 submitNormalModeInputText('quit')
165 expect(pane.destroyActiveItem).toHaveBeenCalled()
166 expect(pane.getItems().length).toBe(1)
168 describe "when the active pane item is modified", ->
170 editor.getBuffer().setText('def')
172 it "opens the prompt to save", ->
173 spyOn(pane, 'promptToSaveItem')
175 submitNormalModeInputText('quit')
176 expect(pane.promptToSaveItem).toHaveBeenCalled()
178 describe ":tabclose", ->
179 it "acts as an alias to :quit", ->
180 spyOn(Ex, 'tabclose').andCallThrough()
181 spyOn(Ex, 'quit').andCallThrough()
183 submitNormalModeInputText('tabclose')
184 expect(Ex.quit).toHaveBeenCalledWith(Ex.tabclose.calls[0].args...)
186 describe ":tabnext", ->
190 pane = atom.workspace.getActivePane()
191 atom.workspace.open().then -> atom.workspace.open()
192 .then -> atom.workspace.open()
194 it "switches to the next tab", ->
195 pane.activateItemAtIndex(1)
197 submitNormalModeInputText('tabnext')
198 expect(pane.getActiveItemIndex()).toBe(2)
200 it "wraps around", ->
201 pane.activateItemAtIndex(pane.getItems().length - 1)
203 submitNormalModeInputText('tabnext')
204 expect(pane.getActiveItemIndex()).toBe(0)
206 describe ":tabprevious", ->
210 pane = atom.workspace.getActivePane()
211 atom.workspace.open().then -> atom.workspace.open()
212 .then -> atom.workspace.open()
214 it "switches to the previous tab", ->
215 pane.activateItemAtIndex(1)
217 submitNormalModeInputText('tabprevious')
218 expect(pane.getActiveItemIndex()).toBe(0)
220 it "wraps around", ->
221 pane.activateItemAtIndex(0)
223 submitNormalModeInputText('tabprevious')
224 expect(pane.getActiveItemIndex()).toBe(pane.getItems().length - 1)
228 spyOn(Ex, 'write').andCallThrough()
231 it "writes the file, then quits", ->
232 spyOn(atom, 'showSaveDialogSync').andReturn(projectPath('wq-1'))
234 submitNormalModeInputText('wq')
235 expect(Ex.write).toHaveBeenCalled()
236 # Since `:wq` only calls `:quit` after `:write` is finished, we need to
237 # wait a bit for the `:quit` call to occur
238 waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100)
240 it "doesn't quit when the file is new and no path is specified in the save dialog", ->
241 spyOn(atom, 'showSaveDialogSync').andReturn(undefined)
243 submitNormalModeInputText('wq')
244 expect(Ex.write).toHaveBeenCalled()
246 # FIXME: This seems dangerous, but setTimeout somehow doesn't work.
248 wasNotCalled = not Ex.quit.wasCalled))
249 waitsFor((-> wasNotCalled), 100)
251 it "passes the file name", ->
253 submitNormalModeInputText('wq wq-2')
256 expect(Ex.write.calls[0].args[1].trim()).toEqual('wq-2')
257 waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100)
260 it "acts as an alias to :wq", ->
263 submitNormalModeInputText('xit')
264 expect(Ex.wq).toHaveBeenCalled()
267 describe "without a file name", ->
268 it "reloads the file from the disk", ->
269 filePath = projectPath("edit-1")
270 editor.getBuffer().setText('abc')
271 editor.saveAs(filePath)
272 fs.writeFileSync(filePath, 'def')
274 submitNormalModeInputText('edit')
275 # Reloading takes a bit
276 waitsFor((-> editor.getText() is 'def'),
277 "the editor's content to change", 100)
279 it "doesn't reload when the file has been modified", ->
280 filePath = projectPath("edit-2")
281 editor.getBuffer().setText('abc')
282 editor.saveAs(filePath)
283 editor.getBuffer().setText('abcd')
284 fs.writeFileSync(filePath, 'def')
286 submitNormalModeInputText('edit')
287 expect(atom.notifications.notifications[0].message).toEqual(
288 'Command error: No write since last change (add ! to override)')
290 setImmediate(-> isntDef = editor.getText() isnt 'def')
291 waitsFor((-> isntDef), "the editor's content not to change", 50)
293 it "reloads when the file has been modified and it is forced", ->
294 filePath = projectPath("edit-3")
295 editor.getBuffer().setText('abc')
296 editor.saveAs(filePath)
297 editor.getBuffer().setText('abcd')
298 fs.writeFileSync(filePath, 'def')
300 submitNormalModeInputText('edit!')
301 expect(atom.notifications.notifications.length).toBe(0)
302 waitsFor((-> editor.getText() is 'def')
303 "the editor's content to change", 50)
305 it "throws an error when editing a new file", ->
306 editor.getBuffer().reload()
308 submitNormalModeInputText('edit')
309 expect(atom.notifications.notifications[0].message).toEqual(
310 'Command error: No file name')
311 atom.commands.dispatch(editorElement, 'ex-mode:open')
312 submitNormalModeInputText('edit!')
313 expect(atom.notifications.notifications[1].message).toEqual(
314 'Command error: No file name')
316 describe "with a file name", ->
318 spyOn(atom.workspace, 'open')
319 editor.getBuffer().reload()
321 it "opens the specified path", ->
322 filePath = projectPath('edit-new-test')
324 submitNormalModeInputText("edit #{filePath}")
325 expect(atom.workspace.open).toHaveBeenCalledWith(filePath)
327 it "opens a relative path", ->
329 submitNormalModeInputText('edit edit-relative-test')
330 expect(atom.workspace.open).toHaveBeenCalledWith(
331 projectPath('edit-relative-test'))
333 it "throws an error if trying to open more than one file", ->
335 submitNormalModeInputText('edit edit-new-test-1 edit-new-test-2')
336 expect(atom.workspace.open.callCount).toBe(0)
337 expect(atom.notifications.notifications[0].message).toEqual(
338 'Command error: Only one file name allowed')
340 describe ":tabedit", ->
341 it "acts as an alias to :edit if supplied with a path", ->
342 spyOn(Ex, 'tabedit').andCallThrough()
345 submitNormalModeInputText('tabedit tabedit-test')
346 expect(Ex.edit).toHaveBeenCalledWith(Ex.tabedit.calls[0].args...)
348 it "acts as an alias to :tabnew if not supplied with a path", ->
349 spyOn(Ex, 'tabedit').andCallThrough()
352 submitNormalModeInputText('tabedit ')
354 .toHaveBeenCalledWith(Ex.tabedit.calls[0].args...)
356 describe ":tabnew", ->
357 it "opens a new tab", ->
358 spyOn(atom.workspace, 'open')
360 submitNormalModeInputText('tabnew')
361 expect(atom.workspace.open).toHaveBeenCalled()
363 describe ":split", ->
364 it "splits the current file upwards", ->
365 pane = atom.workspace.getActivePane()
366 spyOn(pane, 'splitUp').andCallThrough()
367 filePath = projectPath('split')
368 editor.saveAs(filePath)
370 submitNormalModeInputText('split')
371 expect(pane.splitUp).toHaveBeenCalled()
372 # FIXME: Should test whether the new pane contains a TextEditor
373 # pointing to the same path
375 describe ":vsplit", ->
376 it "splits the current file to the left", ->
377 pane = atom.workspace.getActivePane()
378 spyOn(pane, 'splitLeft').andCallThrough()
379 filePath = projectPath('vsplit')
380 editor.saveAs(filePath)
382 submitNormalModeInputText('vsplit')
383 expect(pane.splitLeft).toHaveBeenCalled()
384 # FIXME: Should test whether the new pane contains a TextEditor
385 # pointing to the same path
387 describe ":delete", ->
389 editor.setText('abc\ndef\nghi\njkl')
390 editor.setCursorBufferPosition([2, 0])
392 it "deletes the current line", ->
394 submitNormalModeInputText('delete')
395 expect(editor.getText()).toEqual('abc\ndef\njkl')
397 it "deletes the lines in the given range", ->
398 processedOpStack = false
399 exState.onDidProcessOpStack -> processedOpStack = true
401 submitNormalModeInputText('1,2delete')
402 expect(editor.getText()).toEqual('ghi\njkl')
404 waitsFor -> processedOpStack
405 editor.setText('abc\ndef\nghi\njkl')
406 editor.setCursorBufferPosition([1, 1])
407 # For some reason, keydown(':') doesn't work here :/
408 atom.commands.dispatch(editorElement, 'ex-mode:open')
409 submitNormalModeInputText(',/k/delete')
410 expect(editor.getText()).toEqual('abc\n')
412 it "undos deleting several lines at once", ->
414 submitNormalModeInputText('-1,.delete')
415 expect(editor.getText()).toEqual('abc\njkl')
416 atom.commands.dispatch(editorElement, 'core:undo')
417 expect(editor.getText()).toEqual('abc\ndef\nghi\njkl')
419 describe ":substitute", ->
421 editor.setText('abcaABC\ndefdDEF\nabcaABC')
422 editor.setCursorBufferPosition([0, 0])
424 it "replaces a character on the current line", ->
426 submitNormalModeInputText(':substitute /a/x')
427 expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC')
429 it "doesn't need a space before the arguments", ->
431 submitNormalModeInputText(':substitute/a/x')
432 expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC')
434 it "respects modifiers passed to it", ->
436 submitNormalModeInputText(':substitute/a/x/g')
437 expect(editor.getText()).toEqual('xbcxABC\ndefdDEF\nabcaABC')
439 atom.commands.dispatch(editorElement, 'ex-mode:open')
440 submitNormalModeInputText(':substitute/a/x/gi')
441 expect(editor.getText()).toEqual('xbcxxBC\ndefdDEF\nabcaABC')
443 it "replaces on multiple lines", ->
445 submitNormalModeInputText(':%substitute/abc/ghi')
446 expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nghiaABC')
448 atom.commands.dispatch(editorElement, 'ex-mode:open')
449 submitNormalModeInputText(':%substitute/abc/ghi/ig')
450 expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nghiaghi')
452 it "can't be delimited by letters", ->
454 submitNormalModeInputText(':substitute nanxngi')
455 expect(atom.notifications.notifications[0].message).toEqual(
456 "Command error: Regular expressions can't be delimited by letters")
457 expect(editor.getText()).toEqual('abcaABC\ndefdDEF\nabcaABC')
459 describe "capturing groups", ->
461 editor.setText('abcaABC\ndefdDEF\nabcaABC')
463 it "replaces \\1 with the first group", ->
465 submitNormalModeInputText(':substitute/bc(.{2})/X\\1X')
466 expect(editor.getText()).toEqual('aXaAXBC\ndefdDEF\nabcaABC')
468 it "replaces multiple groups", ->
470 submitNormalModeInputText(':substitute/a([a-z]*)aA([A-Z]*)/X\\1XY\\2Y')
471 expect(editor.getText()).toEqual('XbcXYBCY\ndefdDEF\nabcaABC')
473 it "replaces \\0 with the entire match", ->
475 submitNormalModeInputText(':substitute/ab(ca)AB/X\\0X')
476 expect(editor.getText()).toEqual('XabcaABXC\ndefdDEF\nabcaABC')
479 it "throws an error without a specified option", ->
481 submitNormalModeInputText(':set')
482 expect(atom.notifications.notifications[0].message).toEqual(
483 'Command error: No option specified')
485 it "sets multiple options at once", ->
486 atom.config.set('editor.showInvisibles', false)
487 atom.config.set('editor.showLineNumbers', false)
489 submitNormalModeInputText(':set list number')
490 expect(atom.config.get('editor.showInvisibles')).toBe(true)
491 expect(atom.config.get('editor.showLineNumbers')).toBe(true)
493 describe "the options", ->
495 atom.config.set('editor.showInvisibles', false)
496 atom.config.set('editor.showLineNumbers', false)
498 it "sets (no)list", ->
500 submitNormalModeInputText(':set list')
501 expect(atom.config.get('editor.showInvisibles')).toBe(true)
502 atom.commands.dispatch(editorElement, 'ex-mode:open')
503 submitNormalModeInputText(':set nolist')
504 expect(atom.config.get('editor.showInvisibles')).toBe(false)
506 it "sets (no)nu(mber)", ->
508 submitNormalModeInputText(':set nu')
509 expect(atom.config.get('editor.showLineNumbers')).toBe(true)
510 atom.commands.dispatch(editorElement, 'ex-mode:open')
511 submitNormalModeInputText(':set nonu')
512 expect(atom.config.get('editor.showLineNumbers')).toBe(false)
513 atom.commands.dispatch(editorElement, 'ex-mode:open')
514 submitNormalModeInputText(':set number')
515 expect(atom.config.get('editor.showLineNumbers')).toBe(true)
516 atom.commands.dispatch(editorElement, 'ex-mode:open')
517 submitNormalModeInputText(':set nonumber')
518 expect(atom.config.get('editor.showLineNumbers')).toBe(false)