]> git.r.bdr.sh - rbdr/dotfiles/blob - atom/packages/ex-mode/spec/ex-commands-spec.coffee
Update vim plugins
[rbdr/dotfiles] / atom / packages / ex-mode / spec / ex-commands-spec.coffee
1 fs = require 'fs-plus'
2 path = require 'path'
3 os = require 'os'
4 uuid = require 'node-uuid'
5 helpers = require './spec-helper'
6
7 Ex = require('../lib/ex').singleton()
8
9 describe "the commands", ->
10 [editor, editorElement, vimState, exState, dir, dir2] = []
11 projectPath = (fileName) -> path.join(dir, fileName)
12 beforeEach ->
13 vimMode = atom.packages.loadPackage('vim-mode')
14 exMode = atom.packages.loadPackage('ex-mode')
15 exMode.activate()
16
17 waitsForPromise ->
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()}")
22 fs.makeTreeSync(dir)
23 fs.makeTreeSync(dir2)
24 atom.project.setPaths([dir, dir2])
25
26 helpers.getEditorElement (element) ->
27 atom.commands.dispatch(element, 'ex-mode:open')
28 keydown('escape')
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")
36
37 afterEach ->
38 fs.removeSync(dir)
39 fs.removeSync(dir2)
40
41 keydown = (key, options={}) ->
42 options.element ?= editorElement
43 helpers.keydown(key, options)
44
45 normalModeInputKeydown = (key, opts = {}) ->
46 editor.normalModeInputView.editorElement.getModel().setText(key)
47
48 submitNormalModeInputText = (text) ->
49 commandEditor = editor.normalModeInputView.editorElement
50 commandEditor.getModel().setText(text)
51 atom.commands.dispatch(commandEditor, "core:confirm")
52
53 describe ":write", ->
54 describe "when editing a new file", ->
55 beforeEach ->
56 editor.getBuffer().setText('abc\ndef')
57
58 it "opens the save dialog", ->
59 spyOn(atom, 'showSaveDialogSync')
60 keydown(':')
61 submitNormalModeInputText('write')
62 expect(atom.showSaveDialogSync).toHaveBeenCalled()
63
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)
67 keydown(':')
68 submitNormalModeInputText('write')
69 expect(fs.existsSync(filePath)).toBe(true)
70 expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc\ndef')
71
72 it "saves when a path is specified in the save dialog", ->
73 spyOn(atom, 'showSaveDialogSync').andReturn(undefined)
74 spyOn(fs, 'writeFileSync')
75 keydown(':')
76 submitNormalModeInputText('write')
77 expect(fs.writeFileSync.calls.length).toBe(0)
78
79 describe "when editing an existing file", ->
80 filePath = ''
81 i = 0
82
83 beforeEach ->
84 i++
85 filePath = projectPath("write-#{i}")
86 editor.setText('abc\ndef')
87 editor.saveAs(filePath)
88
89 it "saves the file", ->
90 editor.setText('abc')
91 keydown(':')
92 submitNormalModeInputText('write')
93 expect(fs.readFileSync(filePath, 'utf-8')).toEqual('abc')
94 expect(editor.isModified()).toBe(false)
95
96 describe "with a specified path", ->
97 newPath = ''
98
99 beforeEach ->
100 newPath = path.relative(dir, "#{filePath}.new")
101 editor.getBuffer().setText('abc')
102 keydown(':')
103
104 afterEach ->
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)
111
112 it "saves to the path", ->
113
114 it "expands .", ->
115 newPath = path.join('.', newPath)
116
117 it "expands ..", ->
118 newPath = path.join('..', newPath)
119
120 it "expands ~", ->
121 newPath = path.join('~', newPath)
122
123 it "throws an error with more than one path", ->
124 keydown(':')
125 submitNormalModeInputText('write path1 path2')
126 expect(atom.notifications.notifications[0].message).toEqual(
127 'Command error: Only one file name allowed'
128 )
129
130 describe "when the file already exists", ->
131 existsPath = ''
132
133 beforeEach ->
134 existsPath = projectPath('write-exists')
135 fs.writeFileSync(existsPath, 'abc')
136
137 afterEach ->
138 fs.removeSync(existsPath)
139
140 it "throws an error if the file already exists", ->
141 keydown(':')
142 submitNormalModeInputText("write #{existsPath}")
143 expect(atom.notifications.notifications[0].message).toEqual(
144 'Command error: File exists (add ! to override)'
145 )
146 expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc')
147
148 it "writes if forced with :write!", ->
149 keydown(':')
150 submitNormalModeInputText("write! #{existsPath}")
151 expect(atom.notifications.notifications).toEqual([])
152 expect(fs.readFileSync(existsPath, 'utf-8')).toEqual('abc\ndef')
153
154 describe ":quit", ->
155 pane = null
156 beforeEach ->
157 waitsForPromise ->
158 pane = atom.workspace.getActivePane()
159 spyOn(pane, 'destroyActiveItem').andCallThrough()
160 atom.workspace.open()
161
162 it "closes the active pane item if not modified", ->
163 keydown(':')
164 submitNormalModeInputText('quit')
165 expect(pane.destroyActiveItem).toHaveBeenCalled()
166 expect(pane.getItems().length).toBe(1)
167
168 describe "when the active pane item is modified", ->
169 beforeEach ->
170 editor.getBuffer().setText('def')
171
172 it "opens the prompt to save", ->
173 spyOn(pane, 'promptToSaveItem')
174 keydown(':')
175 submitNormalModeInputText('quit')
176 expect(pane.promptToSaveItem).toHaveBeenCalled()
177
178 describe ":tabclose", ->
179 it "acts as an alias to :quit", ->
180 spyOn(Ex, 'tabclose').andCallThrough()
181 spyOn(Ex, 'quit').andCallThrough()
182 keydown(':')
183 submitNormalModeInputText('tabclose')
184 expect(Ex.quit).toHaveBeenCalledWith(Ex.tabclose.calls[0].args...)
185
186 describe ":tabnext", ->
187 pane = null
188 beforeEach ->
189 waitsForPromise ->
190 pane = atom.workspace.getActivePane()
191 atom.workspace.open().then -> atom.workspace.open()
192 .then -> atom.workspace.open()
193
194 it "switches to the next tab", ->
195 pane.activateItemAtIndex(1)
196 keydown(':')
197 submitNormalModeInputText('tabnext')
198 expect(pane.getActiveItemIndex()).toBe(2)
199
200 it "wraps around", ->
201 pane.activateItemAtIndex(pane.getItems().length - 1)
202 keydown(':')
203 submitNormalModeInputText('tabnext')
204 expect(pane.getActiveItemIndex()).toBe(0)
205
206 describe ":tabprevious", ->
207 pane = null
208 beforeEach ->
209 waitsForPromise ->
210 pane = atom.workspace.getActivePane()
211 atom.workspace.open().then -> atom.workspace.open()
212 .then -> atom.workspace.open()
213
214 it "switches to the previous tab", ->
215 pane.activateItemAtIndex(1)
216 keydown(':')
217 submitNormalModeInputText('tabprevious')
218 expect(pane.getActiveItemIndex()).toBe(0)
219
220 it "wraps around", ->
221 pane.activateItemAtIndex(0)
222 keydown(':')
223 submitNormalModeInputText('tabprevious')
224 expect(pane.getActiveItemIndex()).toBe(pane.getItems().length - 1)
225
226 describe ":wq", ->
227 beforeEach ->
228 spyOn(Ex, 'write').andCallThrough()
229 spyOn(Ex, 'quit')
230
231 it "writes the file, then quits", ->
232 spyOn(atom, 'showSaveDialogSync').andReturn(projectPath('wq-1'))
233 keydown(':')
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)
239
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)
242 keydown(':')
243 submitNormalModeInputText('wq')
244 expect(Ex.write).toHaveBeenCalled()
245 wasNotCalled = false
246 # FIXME: This seems dangerous, but setTimeout somehow doesn't work.
247 setImmediate((->
248 wasNotCalled = not Ex.quit.wasCalled))
249 waitsFor((-> wasNotCalled), 100)
250
251 it "passes the file name", ->
252 keydown(':')
253 submitNormalModeInputText('wq wq-2')
254 expect(Ex.write)
255 .toHaveBeenCalled()
256 expect(Ex.write.calls[0].args[1].trim()).toEqual('wq-2')
257 waitsFor((-> Ex.quit.wasCalled), "the :quit command to be called", 100)
258
259 describe ":xit", ->
260 it "acts as an alias to :wq", ->
261 spyOn(Ex, 'wq')
262 keydown(':')
263 submitNormalModeInputText('xit')
264 expect(Ex.wq).toHaveBeenCalled()
265
266 describe ":edit", ->
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')
273 keydown(':')
274 submitNormalModeInputText('edit')
275 # Reloading takes a bit
276 waitsFor((-> editor.getText() is 'def'),
277 "the editor's content to change", 100)
278
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')
285 keydown(':')
286 submitNormalModeInputText('edit')
287 expect(atom.notifications.notifications[0].message).toEqual(
288 'Command error: No write since last change (add ! to override)')
289 isntDef = false
290 setImmediate(-> isntDef = editor.getText() isnt 'def')
291 waitsFor((-> isntDef), "the editor's content not to change", 50)
292
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')
299 keydown(':')
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)
304
305 it "throws an error when editing a new file", ->
306 editor.getBuffer().reload()
307 keydown(':')
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')
315
316 describe "with a file name", ->
317 beforeEach ->
318 spyOn(atom.workspace, 'open')
319 editor.getBuffer().reload()
320
321 it "opens the specified path", ->
322 filePath = projectPath('edit-new-test')
323 keydown(':')
324 submitNormalModeInputText("edit #{filePath}")
325 expect(atom.workspace.open).toHaveBeenCalledWith(filePath)
326
327 it "opens a relative path", ->
328 keydown(':')
329 submitNormalModeInputText('edit edit-relative-test')
330 expect(atom.workspace.open).toHaveBeenCalledWith(
331 projectPath('edit-relative-test'))
332
333 it "throws an error if trying to open more than one file", ->
334 keydown(':')
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')
339
340 describe ":tabedit", ->
341 it "acts as an alias to :edit if supplied with a path", ->
342 spyOn(Ex, 'tabedit').andCallThrough()
343 spyOn(Ex, 'edit')
344 keydown(':')
345 submitNormalModeInputText('tabedit tabedit-test')
346 expect(Ex.edit).toHaveBeenCalledWith(Ex.tabedit.calls[0].args...)
347
348 it "acts as an alias to :tabnew if not supplied with a path", ->
349 spyOn(Ex, 'tabedit').andCallThrough()
350 spyOn(Ex, 'tabnew')
351 keydown(':')
352 submitNormalModeInputText('tabedit ')
353 expect(Ex.tabnew)
354 .toHaveBeenCalledWith(Ex.tabedit.calls[0].args...)
355
356 describe ":tabnew", ->
357 it "opens a new tab", ->
358 spyOn(atom.workspace, 'open')
359 keydown(':')
360 submitNormalModeInputText('tabnew')
361 expect(atom.workspace.open).toHaveBeenCalled()
362
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)
369 keydown(':')
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
374
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)
381 keydown(':')
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
386
387 describe ":delete", ->
388 beforeEach ->
389 editor.setText('abc\ndef\nghi\njkl')
390 editor.setCursorBufferPosition([2, 0])
391
392 it "deletes the current line", ->
393 keydown(':')
394 submitNormalModeInputText('delete')
395 expect(editor.getText()).toEqual('abc\ndef\njkl')
396
397 it "deletes the lines in the given range", ->
398 processedOpStack = false
399 exState.onDidProcessOpStack -> processedOpStack = true
400 keydown(':')
401 submitNormalModeInputText('1,2delete')
402 expect(editor.getText()).toEqual('ghi\njkl')
403
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')
411
412 it "undos deleting several lines at once", ->
413 keydown(':')
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')
418
419 describe ":substitute", ->
420 beforeEach ->
421 editor.setText('abcaABC\ndefdDEF\nabcaABC')
422 editor.setCursorBufferPosition([0, 0])
423
424 it "replaces a character on the current line", ->
425 keydown(':')
426 submitNormalModeInputText(':substitute /a/x')
427 expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC')
428
429 it "doesn't need a space before the arguments", ->
430 keydown(':')
431 submitNormalModeInputText(':substitute/a/x')
432 expect(editor.getText()).toEqual('xbcaABC\ndefdDEF\nabcaABC')
433
434 it "respects modifiers passed to it", ->
435 keydown(':')
436 submitNormalModeInputText(':substitute/a/x/g')
437 expect(editor.getText()).toEqual('xbcxABC\ndefdDEF\nabcaABC')
438
439 atom.commands.dispatch(editorElement, 'ex-mode:open')
440 submitNormalModeInputText(':substitute/a/x/gi')
441 expect(editor.getText()).toEqual('xbcxxBC\ndefdDEF\nabcaABC')
442
443 it "replaces on multiple lines", ->
444 keydown(':')
445 submitNormalModeInputText(':%substitute/abc/ghi')
446 expect(editor.getText()).toEqual('ghiaABC\ndefdDEF\nghiaABC')
447
448 atom.commands.dispatch(editorElement, 'ex-mode:open')
449 submitNormalModeInputText(':%substitute/abc/ghi/ig')
450 expect(editor.getText()).toEqual('ghiaghi\ndefdDEF\nghiaghi')
451
452 it "can't be delimited by letters", ->
453 keydown(':')
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')
458
459 describe "capturing groups", ->
460 beforeEach ->
461 editor.setText('abcaABC\ndefdDEF\nabcaABC')
462
463 it "replaces \\1 with the first group", ->
464 keydown(':')
465 submitNormalModeInputText(':substitute/bc(.{2})/X\\1X')
466 expect(editor.getText()).toEqual('aXaAXBC\ndefdDEF\nabcaABC')
467
468 it "replaces multiple groups", ->
469 keydown(':')
470 submitNormalModeInputText(':substitute/a([a-z]*)aA([A-Z]*)/X\\1XY\\2Y')
471 expect(editor.getText()).toEqual('XbcXYBCY\ndefdDEF\nabcaABC')
472
473 it "replaces \\0 with the entire match", ->
474 keydown(':')
475 submitNormalModeInputText(':substitute/ab(ca)AB/X\\0X')
476 expect(editor.getText()).toEqual('XabcaABXC\ndefdDEF\nabcaABC')
477
478 describe ":set", ->
479 it "throws an error without a specified option", ->
480 keydown(':')
481 submitNormalModeInputText(':set')
482 expect(atom.notifications.notifications[0].message).toEqual(
483 'Command error: No option specified')
484
485 it "sets multiple options at once", ->
486 atom.config.set('editor.showInvisibles', false)
487 atom.config.set('editor.showLineNumbers', false)
488 keydown(':')
489 submitNormalModeInputText(':set list number')
490 expect(atom.config.get('editor.showInvisibles')).toBe(true)
491 expect(atom.config.get('editor.showLineNumbers')).toBe(true)
492
493 describe "the options", ->
494 beforeEach ->
495 atom.config.set('editor.showInvisibles', false)
496 atom.config.set('editor.showLineNumbers', false)
497
498 it "sets (no)list", ->
499 keydown(':')
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)
505
506 it "sets (no)nu(mber)", ->
507 keydown(':')
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)