whitespace-spec.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. const path = require('path')
  2. const fs = require('fs-plus')
  3. const temp = require('temp')
  4. const {it, fit, ffit, beforeEach} = require('./async-spec-helpers') // eslint-disable-line
  5. describe('Whitespace', () => {
  6. let editor, buffer, workspaceElement
  7. beforeEach(async () => {
  8. const directory = temp.mkdirSync()
  9. atom.project.setPaths([directory])
  10. workspaceElement = atom.views.getView(atom.workspace)
  11. const filePath = path.join(directory, 'atom-whitespace.txt')
  12. fs.writeFileSync(filePath, '')
  13. fs.writeFileSync(path.join(directory, 'sample.txt'), 'Some text.\n')
  14. editor = await atom.workspace.open(filePath)
  15. buffer = editor.getBuffer()
  16. await atom.packages.activatePackage('whitespace')
  17. })
  18. describe('when the editor is destroyed', () => {
  19. beforeEach(() => editor.destroy())
  20. it('does not leak subscriptions', async () => {
  21. const {whitespace} = atom.packages.getActivePackage('whitespace').mainModule
  22. expect(whitespace.subscriptions.disposables.size).toBe(2)
  23. await atom.packages.deactivatePackage('whitespace')
  24. expect(whitespace.subscriptions.disposables).toBeNull()
  25. })
  26. })
  27. describe("when 'whitespace.removeTrailingWhitespace' is true", () => {
  28. beforeEach(() => atom.config.set('whitespace.removeTrailingWhitespace', true))
  29. it('strips trailing whitespace before an editor saves a buffer', async () => {
  30. // works for buffers that are already open when package is initialized
  31. editor.insertText('foo \nbar\t \n\nbaz\n')
  32. await editor.save()
  33. expect(editor.getText()).toBe('foo\nbar\n\nbaz\n')
  34. editor = await atom.workspace.open('sample.txt')
  35. editor.moveToEndOfLine()
  36. editor.insertText(' ')
  37. // move cursor to next line to avoid ignoreWhitespaceOnCurrentLine
  38. editor.moveToBottom()
  39. await editor.save()
  40. expect(editor.getText()).toBe('Some text.\n')
  41. })
  42. it('works for files with CRLF line endings', async () => {
  43. editor.insertText('foo \r\nbar\t \r\n\r\nbaz\r\n')
  44. await editor.save()
  45. expect(editor.getText()).toBe('foo\r\nbar\r\n\r\nbaz\r\n')
  46. })
  47. it('clears blank lines when the editor inserts a newline', () => {
  48. // Need autoIndent to be true
  49. editor.update({autoIndent: true})
  50. // Create an indent level and insert a newline
  51. editor.setIndentationForBufferRow(0, 1)
  52. editor.insertText('\n')
  53. expect(editor.getText()).toBe('\n ')
  54. // Undo the newline insert and redo it
  55. editor.undo()
  56. expect(editor.getText()).toBe(' ')
  57. editor.redo()
  58. expect(editor.getText()).toBe('\n ')
  59. // Test for multiple cursors, possibly without blank lines
  60. editor.insertText('foo')
  61. editor.insertText('\n')
  62. editor.setCursorBufferPosition([1, 5]) // Cursor after 'foo'
  63. editor.addCursorAtBufferPosition([2, 2]) // Cursor on the next line (blank)
  64. editor.insertText('\n')
  65. expect(editor.getText()).toBe('\n foo\n \n\n ')
  66. })
  67. })
  68. describe("when 'whitespace.removeTrailingWhitespace' is false", () => {
  69. beforeEach(() => atom.config.set('whitespace.removeTrailingWhitespace', false))
  70. it('does not trim trailing whitespace', async () => {
  71. editor.insertText("don't trim me \n\n")
  72. await editor.save()
  73. expect(editor.getText()).toBe("don't trim me \n")
  74. })
  75. describe('when the setting is set scoped to the grammar', () => {
  76. beforeEach(() => {
  77. atom.config.set('whitespace.removeTrailingWhitespace', true)
  78. atom.config.set('whitespace.removeTrailingWhitespace', false, {scopeSelector: '.text.plain'})
  79. })
  80. it('does not trim trailing whitespace', async () => {
  81. editor.insertText("don't trim me \n\n")
  82. await editor.save()
  83. expect(editor.getText()).toBe("don't trim me \n")
  84. })
  85. })
  86. })
  87. describe("when 'whitespace.ignoreWhitespaceOnCurrentLine' is true", () => {
  88. beforeEach(() => atom.config.set('whitespace.ignoreWhitespaceOnCurrentLine', true))
  89. describe('respects multiple cursors', () => {
  90. it('removes the whitespace from all lines, excluding the current lines', async () => {
  91. editor.insertText('1 \n2 \n3 \n')
  92. editor.setCursorBufferPosition([1, 3])
  93. editor.addCursorAtBufferPosition([2, 3])
  94. await editor.save()
  95. expect(editor.getText()).toBe('1\n2 \n3 \n')
  96. })
  97. })
  98. describe('when buffer is opened in multiple editors', () => {
  99. let editor2
  100. beforeEach(async () => {
  101. editor2 = atom.workspace.buildTextEditor({buffer: editor.buffer})
  102. await atom.workspace.open(editor2)
  103. })
  104. it('[editor is activeEditor] remove WS with excluding active editor\'s cursor line', async () => {
  105. editor.insertText('1 \n2 \n3 \n')
  106. editor.setCursorBufferPosition([1, 3])
  107. editor2.setCursorBufferPosition([2, 3])
  108. atom.workspace.getActivePane().activateItem(editor)
  109. expect(atom.workspace.getActiveTextEditor()).toBe(editor)
  110. await editor.save()
  111. expect(editor.getText()).toBe('1\n2 \n3\n')
  112. })
  113. it('[editor2 is activeEditor] remove WS with excluding active editor\'s cursor line', async () => {
  114. editor.insertText('1 \n2 \n3 \n')
  115. editor.setCursorBufferPosition([1, 3])
  116. editor2.setCursorBufferPosition([2, 3])
  117. atom.workspace.getActivePane().activateItem(editor2)
  118. expect(atom.workspace.getActiveTextEditor()).toBe(editor2)
  119. await editor2.save()
  120. expect(editor.getText()).toBe('1\n2\n3 \n')
  121. })
  122. it('[either editor nor editor2 is activeEditor] remove WS but doesn\'t exclude cursor line for non active editor', async () => {
  123. editor.insertText('1 \n2 \n3 \n')
  124. editor.setCursorBufferPosition([1, 3])
  125. editor2.setCursorBufferPosition([1, 3])
  126. const editor3 = await atom.workspace.open()
  127. expect(atom.workspace.getActiveTextEditor()).toBe(editor3)
  128. await editor.save()
  129. expect(editor.getText()).toBe('1\n2\n3\n') // all trainling-WS were removed
  130. })
  131. })
  132. })
  133. describe("when 'whitespace.ignoreWhitespaceOnCurrentLine' is false", () => {
  134. beforeEach(() => atom.config.set('whitespace.ignoreWhitespaceOnCurrentLine', false))
  135. it('removes the whitespace from all lines, including the current lines', async () => {
  136. editor.insertText('1 \n2 \n3 \n')
  137. editor.setCursorBufferPosition([1, 3])
  138. editor.addCursorAtBufferPosition([2, 3])
  139. await editor.save()
  140. expect(editor.getText()).toBe('1\n2\n3\n')
  141. })
  142. })
  143. describe("when 'whitespace.ignoreWhitespaceOnlyLines' is false", () => {
  144. beforeEach(() => atom.config.set('whitespace.ignoreWhitespaceOnlyLines', false))
  145. it('removes the whitespace from all lines, including the whitespace-only lines', async () => {
  146. editor.insertText('1 \n2\t \n\t \n3\n')
  147. // move cursor to bottom for preventing effect of whitespace.ignoreWhitespaceOnCurrentLine
  148. editor.moveToBottom()
  149. await editor.save()
  150. expect(editor.getText()).toBe('1\n2\n\n3\n')
  151. })
  152. })
  153. describe("when 'whitespace.ignoreWhitespaceOnlyLines' is true", () => {
  154. beforeEach(() => atom.config.set('whitespace.ignoreWhitespaceOnlyLines', true))
  155. it('removes the whitespace from all lines, excluding the whitespace-only lines', async () => {
  156. editor.insertText('1 \n2\t \n\t \n3\n')
  157. // move cursor to bottom for preventing effect of whitespace.ignoreWhitespaceOnCurrentLine
  158. editor.moveToBottom()
  159. await editor.save()
  160. expect(editor.getText()).toBe('1\n2\n\t \n3\n')
  161. })
  162. })
  163. describe("when 'whitespace.ensureSingleTrailingNewline' is true", () => {
  164. beforeEach(() => atom.config.set('whitespace.ensureSingleTrailingNewline', true))
  165. it('adds a trailing newline when there is no trailing newline', async () => {
  166. editor.insertText('foo')
  167. await editor.save()
  168. expect(editor.getText()).toBe('foo\n')
  169. })
  170. it('removes extra trailing newlines and only keeps one', async () => {
  171. editor.insertText('foo\n\n\n\n')
  172. await editor.save()
  173. expect(editor.getText()).toBe('foo\n')
  174. })
  175. it('leaves a buffer with a single trailing newline untouched', async () => {
  176. editor.insertText('foo\nbar\n')
  177. await editor.save()
  178. expect(editor.getText()).toBe('foo\nbar\n')
  179. })
  180. it('leaves an empty buffer untouched', () => {
  181. editor.insertText('')
  182. editor.save()
  183. expect(editor.getText()).toBe('')
  184. })
  185. it('leaves a buffer that is a single newline untouched', () => {
  186. editor.insertText('\n')
  187. editor.save()
  188. expect(editor.getText()).toBe('\n')
  189. })
  190. it('does not move the cursor when the new line is added', async () => {
  191. editor.insertText('foo\nboo')
  192. editor.setCursorBufferPosition([0, 3])
  193. await editor.save()
  194. expect(editor.getText()).toBe('foo\nboo\n')
  195. expect(editor.getCursorBufferPosition()).toEqual([0, 3])
  196. })
  197. it('preserves selections when saving on last line', async () => {
  198. editor.insertText('foo')
  199. editor.setCursorBufferPosition([0, 0])
  200. editor.selectToEndOfLine()
  201. const originalSelectionRange = editor.getLastSelection().getBufferRange()
  202. await editor.save()
  203. const newSelectionRange = editor.getLastSelection().getBufferRange()
  204. expect(originalSelectionRange).toEqual(newSelectionRange)
  205. })
  206. })
  207. describe("when 'whitespace.ensureSingleTrailingNewline' is false", () => {
  208. beforeEach(() => atom.config.set('whitespace.ensureSingleTrailingNewline', false))
  209. it('does not add trailing newline if ensureSingleTrailingNewline is false', () => {
  210. editor.insertText('no trailing newline')
  211. editor.save()
  212. expect(editor.getText()).toBe('no trailing newline')
  213. })
  214. })
  215. describe('GFM whitespace trimming', () => {
  216. describe('when keepMarkdownLineBreakWhitespace is true', () => {
  217. beforeEach(() => {
  218. atom.config.set('whitespace.removeTrailingWhitespace', true)
  219. atom.config.set('whitespace.ignoreWhitespaceOnCurrentLine', false)
  220. atom.config.set('whitespace.keepMarkdownLineBreakWhitespace', true)
  221. waitsForPromise(() => atom.packages.activatePackage('language-gfm'))
  222. runs(() => editor.setGrammar(atom.grammars.grammarForScopeName('source.gfm')))
  223. })
  224. it('trims GFM text with a single space', async () => {
  225. editor.insertText('foo \nline break!')
  226. await editor.save()
  227. expect(editor.getText()).toBe('foo\nline break!\n')
  228. })
  229. it('leaves GFM text with double spaces alone', async () => {
  230. editor.insertText('foo \nline break!')
  231. await editor.save()
  232. expect(editor.getText()).toBe('foo \nline break!\n')
  233. })
  234. it('leaves GFM text with a more than two spaces', async () => {
  235. editor.insertText('foo \nline break!')
  236. await editor.save()
  237. expect(editor.getText()).toBe('foo \nline break!\n')
  238. })
  239. it('trims empty lines', async () => {
  240. editor.insertText('foo\n ')
  241. await editor.save()
  242. expect(editor.getText()).toBe('foo\n')
  243. editor.setText('foo\n ')
  244. await editor.save()
  245. expect(editor.getText()).toBe('foo\n')
  246. })
  247. it("respects 'whitespace.ignoreWhitespaceOnCurrentLine' setting", async () => {
  248. atom.config.set('whitespace.ignoreWhitespaceOnCurrentLine', true)
  249. editor.insertText('foo \nline break!')
  250. editor.setCursorBufferPosition([0, 4])
  251. await editor.save()
  252. expect(editor.getText()).toBe('foo \nline break!\n')
  253. })
  254. it("respects 'whitespace.ignoreWhitespaceOnlyLines' setting", async () => {
  255. atom.config.set('whitespace.ignoreWhitespaceOnlyLines', true)
  256. editor.insertText('\t \nline break!')
  257. await editor.save()
  258. expect(editor.getText()).toBe('\t \nline break!\n')
  259. })
  260. })
  261. describe('when keepMarkdownLineBreakWhitespace is false', () => {
  262. beforeEach(() => {
  263. atom.config.set('whitespace.ignoreWhitespaceOnCurrentLine', false)
  264. atom.config.set('whitespace.keepMarkdownLineBreakWhitespace', false)
  265. waitsForPromise(() => atom.packages.activatePackage('language-gfm'))
  266. runs(() => editor.setGrammar(atom.grammars.grammarForScopeName('source.gfm')))
  267. })
  268. it('trims GFM text with a single space', async () => {
  269. editor.insertText('foo \nline break!')
  270. await editor.save()
  271. expect(editor.getText()).toBe('foo\nline break!\n')
  272. })
  273. it('trims GFM text with two spaces', async () => {
  274. editor.insertText('foo \nline break!')
  275. await editor.save()
  276. expect(editor.getText()).toBe('foo\nline break!\n')
  277. })
  278. it('trims GFM text with a more than two spaces', async () => {
  279. editor.insertText('foo \nline break!')
  280. await editor.save()
  281. expect(editor.getText()).toBe('foo\nline break!\n')
  282. })
  283. it('trims empty lines', async () => {
  284. editor.insertText('foo\n ')
  285. await editor.save()
  286. expect(editor.getText()).toBe('foo\n')
  287. editor.setText('foo\n ')
  288. await editor.save()
  289. expect(editor.getText()).toBe('foo\n')
  290. })
  291. it("respects 'whitespace.ignoreWhitespaceOnCurrentLine' setting", async () => {
  292. atom.config.set('whitespace.ignoreWhitespaceOnCurrentLine', true)
  293. editor.insertText('foo \nline break!')
  294. editor.setCursorBufferPosition([0, 4])
  295. await editor.save()
  296. expect(editor.getText()).toBe('foo \nline break!\n')
  297. })
  298. it("respects 'whitespace.ignoreWhitespaceOnlyLines' setting", async () => {
  299. atom.config.set('whitespace.ignoreWhitespaceOnlyLines', true)
  300. editor.insertText('\t \nline break!')
  301. await editor.save()
  302. expect(editor.getText()).toBe('\t \nline break!\n')
  303. })
  304. })
  305. })
  306. describe('when the editor is split', () =>
  307. it('does not throw exceptions when the editor is saved after the split is closed (regression)', async () => {
  308. atom.workspace.getActivePane().splitRight({copyActiveItem: true})
  309. atom.workspace.getPanes()[0].destroyItems()
  310. editor = atom.workspace.getActivePaneItem()
  311. editor.setText('test')
  312. await editor.save()
  313. expect(editor.getText()).toBe('test\n')
  314. })
  315. )
  316. describe('when deactivated', () =>
  317. it('does not remove trailing whitespace from editors opened after deactivation', async () => {
  318. atom.config.set('whitespace.removeTrailingWhitespace', true)
  319. await atom.packages.deactivatePackage('whitespace')
  320. editor.setText('foo \n')
  321. await editor.save()
  322. expect(editor.getText()).toBe('foo \n')
  323. await atom.workspace.open('sample2.txt')
  324. editor = atom.workspace.getActiveTextEditor()
  325. editor.setText('foo \n')
  326. await editor.save()
  327. expect(editor.getText()).toBe('foo \n')
  328. })
  329. )
  330. describe("when the 'whitespace:remove-trailing-whitespace' command is run", () => {
  331. beforeEach(() => buffer.setText('foo \nbar\t \n\nbaz'))
  332. it('removes the trailing whitespace in the active editor', () => {
  333. atom.commands.dispatch(workspaceElement, 'whitespace:remove-trailing-whitespace')
  334. expect(buffer.getText()).toBe('foo\nbar\n\nbaz')
  335. })
  336. it('does not attempt to remove whitespace when the package is deactivated', async () => {
  337. await atom.packages.deactivatePackage('whitespace')
  338. expect(buffer.getText()).toBe('foo \nbar\t \n\nbaz')
  339. })
  340. })
  341. describe("when the 'whitespace:save-with-trailing-whitespace' command is run", () => {
  342. beforeEach(() => {
  343. atom.config.set('whitespace.removeTrailingWhitespace', true)
  344. atom.config.set('whitespace.ensureSingleTrailingNewline', false)
  345. buffer.setText('foo \nbar\t \n\nbaz')
  346. })
  347. it('saves the file without removing any trailing whitespace', () =>
  348. waitsFor((done) => {
  349. buffer.onDidSave(() => {
  350. expect(buffer.getText()).toBe('foo \nbar\t \n\nbaz')
  351. expect(buffer.isModified()).toBe(false)
  352. done()
  353. })
  354. atom.commands.dispatch(workspaceElement, 'whitespace:save-with-trailing-whitespace')
  355. })
  356. )
  357. })
  358. describe("when the 'whitespace:save-without-trailing-whitespace' command is run", () => {
  359. beforeEach(() => {
  360. atom.config.set('whitespace.removeTrailingWhitespace', false)
  361. atom.config.set('whitespace.ensureSingleTrailingNewline', false)
  362. buffer.setText('foo \nbar\t \n\nbaz')
  363. })
  364. it('saves the file and removes any trailing whitespace', () =>
  365. waitsFor(function (done) {
  366. buffer.onDidSave(() => {
  367. expect(buffer.getText()).toBe('foo\nbar\n\nbaz')
  368. expect(buffer.isModified()).toBe(false)
  369. done()
  370. })
  371. atom.commands.dispatch(workspaceElement, 'whitespace:save-without-trailing-whitespace')
  372. })
  373. )
  374. })
  375. describe("when the 'whitespace:convert-tabs-to-spaces' command is run", () => {
  376. it('removes leading \\t characters and replaces them with spaces using the configured tab length', () => {
  377. editor.setTabLength(2)
  378. buffer.setText('\ta\n\t\nb\t\nc\t\td')
  379. atom.commands.dispatch(workspaceElement, 'whitespace:convert-tabs-to-spaces')
  380. expect(buffer.getText()).toBe(' a\n \nb\t\nc\t\td')
  381. editor.setTabLength(3)
  382. buffer.setText('\ta\n\t\nb\t\nc\t\td')
  383. atom.commands.dispatch(workspaceElement, 'whitespace:convert-tabs-to-spaces')
  384. expect(buffer.getText()).toBe(' a\n \nb\t\nc\t\td')
  385. })
  386. it('changes the tab type to soft tabs', () => {
  387. atom.commands.dispatch(workspaceElement, 'whitespace:convert-tabs-to-spaces')
  388. expect(editor.getSoftTabs()).toBe(true)
  389. })
  390. })
  391. describe("when the 'whitespace:convert-spaces-to-tabs' command is run", () => {
  392. it('removes leading space characters and replaces them with hard tabs', () => {
  393. editor.setTabLength(2)
  394. buffer.setText(' a\n \nb \nc d')
  395. atom.commands.dispatch(workspaceElement, 'whitespace:convert-spaces-to-tabs')
  396. expect(buffer.getText()).toBe('\t a\n\t\nb \nc d')
  397. editor.setTabLength(3)
  398. buffer.setText(' a\n \nb \nc d')
  399. atom.commands.dispatch(workspaceElement, 'whitespace:convert-spaces-to-tabs')
  400. expect(buffer.getText()).toBe('\t a\n\t\nb \nc d')
  401. })
  402. it('handles mixed runs of tabs and spaces correctly', () => {
  403. editor.setTabLength(4)
  404. buffer.setText(' \t \t\ta ')
  405. atom.commands.dispatch(workspaceElement, 'whitespace:convert-spaces-to-tabs')
  406. expect(buffer.getText()).toBe('\t\t\t\t\ta ')
  407. })
  408. it('changes the tab type to hard tabs', () => {
  409. atom.commands.dispatch(workspaceElement, 'whitespace:convert-spaces-to-tabs')
  410. expect(editor.getSoftTabs()).toBe(false)
  411. })
  412. it("changes the tab length to user's tab-size", () => {
  413. editor.setTabLength(4)
  414. buffer.setText(' ')
  415. atom.commands.dispatch(workspaceElement, 'whitespace:convert-spaces-to-tabs')
  416. expect(editor.getTabLength()).toBe(2)
  417. })
  418. })
  419. describe("when the 'whitespace:convert-all-tabs-to-spaces' command is run", () => {
  420. it('removes all \\t characters and replaces them with spaces using the configured tab length', () => {
  421. editor.setTabLength(2)
  422. buffer.setText('\ta\n\t\nb\t\nc\t\td')
  423. atom.commands.dispatch(workspaceElement, 'whitespace:convert-all-tabs-to-spaces')
  424. expect(buffer.getText()).toBe(' a\n \nb \nc d')
  425. editor.setTabLength(3)
  426. buffer.setText('\ta\n\t\nb\t\nc\t\td')
  427. atom.commands.dispatch(workspaceElement, 'whitespace:convert-all-tabs-to-spaces')
  428. expect(buffer.getText()).toBe(' a\n \nb \nc d')
  429. })
  430. it('changes the tab type to soft tabs', () => {
  431. atom.commands.dispatch(workspaceElement, 'whitespace:convert-all-tabs-to-spaces')
  432. expect(editor.getSoftTabs()).toBe(true)
  433. })
  434. })
  435. })