├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── benchmarks
└── tabs-to-spaces.bench.js
├── coffeelint.json
├── lib
├── index.js
└── tabs-to-spaces.js
├── menus
└── tabs-to-spaces.cson
├── package.json
├── sample
└── jquery-git2.js.txt
└── spec
├── spec-helper.js
└── tabs-to-spaces-spec.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules
4 | doc
5 | docs
6 | performance-results
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 |
3 | notifications:
4 | email:
5 | on_success: never
6 | on_failure: change
7 |
8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh'
9 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## **v1.0.5** — *Released: 2018-04-20*
4 |
5 | * Update engines version so that people on old versions of Atom don't get an incompatible version of tabs-to-spaces
6 |
7 | ## **v1.0.4** — *Deprecated due to a problem with the engines version, see above*
8 |
9 | * [#52](https://github.com/lee-dohm/tabs-to-spaces/issues/52) — Handle change to async file saves in specs
10 | * Fix deprecation of `undo` option
11 |
12 | ## **v1.0.3** — *Released: 2016-12-29*
13 |
14 | * [#49](https://github.com/lee-dohm/tabs-to-spaces/issues/49) — Convert to line-by-line processing for functions that work with leading whitespace because it is somewhat faster
15 | * Convert package code to JavaScript
16 |
17 | ## **v1.0.2** — *Released: 2016-02-15*
18 |
19 | * [#41](https://github.com/lee-dohm/tabs-to-spaces/issues41) — Revert performance improvement from v1.0.0 because it was causing cursors to get pushed to the end of the file when the contents were changed
20 |
21 | ## **v1.0.1** — *Released: 2015-11-19*
22 |
23 | * [#38](https://github.com/lee-dohm/tabs-to-spaces/issues/38) — Fix crash when upgrading the package from inside Atom
24 |
25 | ## **v1.0.0** — *Released: 2015-11-18*
26 |
27 | * *Significant* performance improvement on large files — 2.11s → 0.448s tabifying the test file `sample/jquery-git2.js.txt`
28 | * Cleaned up the specs to make them less brittle
29 | * Updated the `onSave` configuration description to use Markdown for better emphasis
30 |
31 | ## **v0.11.1** — *Released: 2015-08-17*
32 |
33 | * Added a warning to the Settings View description for the `onSave` setting that this setting can significantly impact save performance for large files
34 |
35 | ## **v0.11.0** — *Released: 2015-05-25*
36 |
37 | * Stopped using undocumented interface
38 | * [#28](https://github.com/lee-dohm/tabs-to-spaces/issues/28) — Change the extension of the sample JavaScript file to prevent strange error message
39 |
40 | ## **v0.10.0** — *Released: 2015-05-01*
41 |
42 | * Clean up for Deprecation Day
43 |
44 | ## **v0.9.2** — *Released: 2015-03-30*
45 |
46 | * Added keywords to the `package.json`
47 |
48 | ## **v0.9.1** — *Released: 2015-03-29*
49 |
50 | * [#27](https://github.com/lee-dohm/tabs-to-spaces/pull/27) by [@Hurtak](https://github.com/Hurtak) — Grouped context menu items into a submenu
51 |
52 | ## **v0.9.0** — *Released: 2015-03-18*
53 | ˆ
54 | * [#21](https://github.com/lee-dohm/tabs-to-spaces/issues/21) — Added "Untabify All" command to convert *all* tabs in a document to spaces
55 |
56 | ## **v0.8.1** — *Released: 2015-02-02*
57 |
58 | * Updated to only support post-API-freeze versions of Atom
59 | * Fixed all the latest deprecations
60 |
61 | ## **v0.8.0** — *Released: 2014-12-07*
62 |
63 | * Cleaned up all deprecations
64 |
65 | ## **v0.7.1** — *Released: 2014-10-22*
66 |
67 | * [#11](https://github.com/lee-dohm/tabs-to-spaces/issues/11) - Disable onSave tabification or untabification of `config.cson`
68 |
69 | ## **v0.7.0** — *Released: 2014-10-17*
70 |
71 | * Add support for language-specific configuration for `onSave` [#9](https://github.com/lee-dohm/tabs-to-spaces/issues/9)
72 | * Skipped v0.6.0 due to publishing issue
73 |
74 | ## **v0.5.1** — *Released: 2014-10-05*
75 |
76 | * Use new configuration schema to turn the `onSave` setting into a dropdown
77 |
78 | ## **v0.5.0** — *Released: 2014-10-03*
79 |
80 | * :bug: Fix [#7](https://github.com/lee-dohm/tabs-to-spaces/issues/7) - Handles any mixture of tabs or spaces at the beginning of lines
81 |
82 | ## **v0.4.2** — *Released: 2014-07-29*
83 |
84 | * :bug: Fix [#5](https://github.com/lee-dohm/tabs-to-spaces/issues/5) - Remove dependency on `fs-plus` because it was generating some sort of strange build error
85 |
86 | ## **v0.4.1** — *Released: 2014-07-21*
87 |
88 | * [#4](https://github.com/lee-dohm/tabs-to-spaces/pull/4) by [@Zren](https://github.com/Zren) - Add default value for `onSave` so that it always shows up in the Settings View
89 |
90 | ## **v0.4.0** — *Released: 2014-06-28*
91 |
92 | * Add support for editor-specific tab length settings [#1](https://github.com/lee-dohm/tabs-to-spaces/issues/1)
93 |
94 | ## **v0.3.4** — *Released: 2014-05-25*
95 |
96 | * :bug: Fix [#2](https://github.com/lee-dohm/tabs-to-spaces/issues/2) - Dereference of `null` in `handleEvents()`
97 |
98 | ## **v0.3.3** — *Released: 2014-05-24*
99 |
100 | * :bug: Fixed the on save event handlers
101 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # CONTRIBUTING
2 |
3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
4 |
5 | These are just guidelines, not rules, use your best judgement and feel free to propose changes to this document in a pull request.
6 |
7 | ## Submitting Issues
8 |
9 | * Check the [debugging guide](https://atom.io/docs/latest/debugging) for tips on debugging. You might be able to find the cause of the problem and fix things yourself.
10 | * Include the version of Atom you are using and the OS.
11 | * Include screenshots and animated GIFs whenever possible; they are immensely helpful.
12 | * Include the behavior you expected.
13 | * Check the dev tools (Cmd+Alt+I) for errors to include. If the dev tools are open *before* the error is triggered, a full stack trace for the error will be logged. If you can reproduce the error, use this approach to get the full stack trace and include it in the issue.
14 | * Perform a cursory search to see if a similar issue has already been submitted.
15 | * Please setup a [profile picture](https://help.github.com/articles/how-do-i-set-up-my-profile-picture) to make yourself recognizable and so we can all get to know each other better.
16 |
17 | ## Pull Requests
18 |
19 | * Include screenshots and animated GIFs in your pull request whenever possible
20 | * Follow the [CoffeeScript](#coffeescript-styleguide), [JavaScript](https://github.com/styleguide/javascript), and [CSS](https://github.com/styleguide/css) styleguides
21 | * Include thoughtfully-worded, well-structured [Jasmine](http://jasmine.github.io/) specs
22 | * Document new code based on the [Documentation Styleguide](#documentation-styleguide)
23 | * End files with a newline
24 | * Place requires in the following order:
25 | * Built in Node Modules (such as `path`)
26 | * Built in Atom and Atom Shell Modules (such as `atom`, `shell`)
27 | * Local Modules (using relative paths)
28 | * Place class properties in the following order:
29 | * Class methods and properties (methods starting with a `@`)
30 | * Instance methods and properties
31 | * Avoid platform-dependent code:
32 | * Use `require('atom').fs.getHomeDirectory()` to get the home directory.
33 | * Use `path.join()` to concatenate filenames.
34 | * Use `os.tmpdir()` rather than `/tmp` when you need to reference the temporary directory.
35 | * Use a plain `return` when returning explicitly at the end of a function.
36 | * Not `return null`, `return undefined`, `null`, or `undefined`
37 |
38 | ## Git Commit Messages
39 |
40 | * Use the present tense ("Add feature" not "Added feature")
41 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
42 | * Limit the first line to 72 characters or less
43 | * Reference issues and pull requests liberally
44 | * Consider starting the commit message with an applicable emoji:
45 | * :lipstick: `:lipstick:` when improving the format/structure of the code
46 | * :racehorse: `:racehorse:` when improving performance
47 | * :non-potable_water: `:non-potable_water:` when plugging memory leaks
48 | * :memo: `:memo:` when writing docs
49 | * :penguin: `:penguin:` when fixing something on Linux
50 | * :apple: `:apple:` when fixing something on Mac OS
51 | * :checkered_flag: `:checkered_flag:` when fixing something on Windows
52 | * :bug: `:bug:` when fixing a bug
53 | * :fire: `:fire:` when removing code or files
54 | * :green_heart: `:green_heart:` when fixing the CI build
55 | * :white_check_mark: `:white_check_mark:` when adding tests
56 | * :lock: `:lock:` when dealing with security
57 | * :arrow_up: `:arrow_up:` when upgrading dependencies
58 |
59 | ## CoffeeScript Styleguide
60 |
61 | * Use parentheses if it improves code clarity
62 | * Prefer alphabetic keywords to symbolic keywords:
63 | * `a is b` instead of `a == b`
64 | * Avoid spaces inside the curly-braces of hash literals:
65 | * `{a: 1, b: 2}` instead of `{ a: 1, b: 2 }`
66 | * Include a single line of whitespace between methods
67 |
68 | ## Documentation Styleguide
69 |
70 | * Use [AtomDoc](https://github.com/atom/atomdoc).
71 | * Use [Markdown](https://daringfireball.net/projects/markdown).
72 | * Reference methods and classes in markdown with the custom `{}` notation:
73 | * Reference classes with `{ClassName}`
74 | * Reference instance methods with `{ClassName::methodName}`
75 | * Reference class methods with `{ClassName.methodName}`
76 |
77 | ### Example
78 |
79 | ```coffee
80 | # Public: Disable the package with the given name.
81 | #
82 | # * `name` The {String} name of the package to disable.
83 | # * `options` (optional) The {Object} with disable options (default: {}):
84 | # * `trackTime` A {Boolean}, `true` to track the amount of time taken.
85 | # * `ignoreErrors` A {Boolean}, `true` to catch and ignore errors thrown.
86 | # * `callback` The {Function} to call after the package has been disabled.
87 | #
88 | # Returns `undefined`.
89 | disablePackage: (name, options, callback) ->
90 | ```
91 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2014-2016, 2018 [Lee Dohm](http://www.lee-dohm.com) and [Lifted Studios](http://www.liftedstudios.com)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tabs to Spaces
2 |
3 | [](https://travis-ci.org/lee-dohm/tabs-to-spaces)
4 | [](https://atom.io/packages/tabs-to-spaces)
5 | [](https://atom.io/packages/tabs-to-spaces)
6 | [](https://david-dm.org/lee-dohm/tabs-to-spaces)
7 |
8 | An Atom package for converting leading whitespace to either all spaces or all tabs.
9 |
10 | ## Usage
11 |
12 | It can convert any form of leading whitespace to either all spaces (Untabify) or the maximum number of tabs and minimum number of spaces with tabs up front (Tabify) to fill the same space. It can also convert all tabs in a document to spaces (Untabify All).
13 |
14 | It will also, with configuration, convert to your preferred method of leading whitespace on save.
15 |
16 | ### Commands
17 |
18 | * `tabs-to-spaces:tabify` — Converts leading whitespace to tabs
19 | * `tabs-to-spaces:untabify` — Converts leading whitespace to spaces
20 | * `tabs-to-spaces:untabify-all` — Converts all whitespace on a line to spaces
21 |
22 | ### Configuration
23 |
24 | Tabs to Spaces uses the following configuration values:
25 |
26 | * `editor.tabLength` — sets the number of space characters a tab character is equivalent to
27 | * `tabs-to-spaces.onSave` — if set to either `tabify` or `untabify` it performs that operation on save. :rotating_light: **Warning:** :rotating_light: Setting this to anything other than `none` can **significantly** impact performance when saving large files.
28 |
29 | The package also supports language-specific configuration for the `onSave` setting. For example, the following configuration will tabify all file types on save except for JavaScript files:
30 |
31 | ```coffee
32 | '*':
33 | 'tabs-to-spaces':
34 | 'onSave': 'tabify'
35 | '.source.js':
36 | 'tabs-to-spaces':
37 | 'onSave': 'none'
38 | ```
39 |
40 | No matter what `tabs-to-spaces.onSave` settings you configure, your `config.cson` will not be automatically tabified or untabified.
41 |
42 | ### Keybindings
43 |
44 | Keybindings have not been set for this package. They can easily be added by referencing the commands listed above.
45 |
46 | ## License
47 |
48 | [MIT](LICENSE.md)
49 |
--------------------------------------------------------------------------------
/benchmarks/tabs-to-spaces.bench.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import fs from 'fs'
4 | import path from 'path'
5 |
6 | export default async function () {
7 | const tabsToSpaces = require('../lib/tabs-to-spaces')
8 | await atom.workspace.open(path.join(__dirname, '../sample/jquery-git2.js.txt'))
9 | let editor = atom.workspace.getActiveTextEditor()
10 |
11 | const t0 = window.performance.now()
12 |
13 | tabsToSpaces.replaceWhitespaceWithTabs(editor)
14 |
15 | const t1 = window.performance.now()
16 |
17 | return [
18 | { name: 'tabify', duration: t1 - t0 }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "coffeescript_error": {
3 | "level": "error"
4 | },
5 | "arrow_spacing": {
6 | "name": "arrow_spacing",
7 | "level": "ignore"
8 | },
9 | "no_tabs": {
10 | "name": "no_tabs",
11 | "level": "error"
12 | },
13 | "no_trailing_whitespace": {
14 | "name": "no_trailing_whitespace",
15 | "level": "error",
16 | "allowed_in_comments": false,
17 | "allowed_in_empty_lines": true
18 | },
19 | "max_line_length": {
20 | "name": "max_line_length",
21 | "value": 100,
22 | "level": "error",
23 | "limitComments": true
24 | },
25 | "line_endings": {
26 | "name": "line_endings",
27 | "level": "ignore",
28 | "value": "unix"
29 | },
30 | "no_trailing_semicolons": {
31 | "name": "no_trailing_semicolons",
32 | "level": "error"
33 | },
34 | "indentation": {
35 | "name": "indentation",
36 | "value": 2,
37 | "level": "error"
38 | },
39 | "camel_case_classes": {
40 | "name": "camel_case_classes",
41 | "level": "error"
42 | },
43 | "colon_assignment_spacing": {
44 | "name": "colon_assignment_spacing",
45 | "level": "ignore",
46 | "spacing": {
47 | "left": 0,
48 | "right": 0
49 | }
50 | },
51 | "no_implicit_braces": {
52 | "name": "no_implicit_braces",
53 | "level": "ignore",
54 | "strict": true
55 | },
56 | "no_plusplus": {
57 | "name": "no_plusplus",
58 | "level": "ignore"
59 | },
60 | "no_throwing_strings": {
61 | "name": "no_throwing_strings",
62 | "level": "error"
63 | },
64 | "no_backticks": {
65 | "name": "no_backticks",
66 | "level": "error"
67 | },
68 | "no_implicit_parens": {
69 | "name": "no_implicit_parens",
70 | "level": "ignore"
71 | },
72 | "no_empty_param_list": {
73 | "name": "no_empty_param_list",
74 | "level": "ignore"
75 | },
76 | "no_stand_alone_at": {
77 | "name": "no_stand_alone_at",
78 | "level": "ignore"
79 | },
80 | "space_operators": {
81 | "name": "space_operators",
82 | "level": "ignore"
83 | },
84 | "duplicate_key": {
85 | "name": "duplicate_key",
86 | "level": "error"
87 | },
88 | "empty_constructor_needs_parens": {
89 | "name": "empty_constructor_needs_parens",
90 | "level": "ignore"
91 | },
92 | "cyclomatic_complexity": {
93 | "name": "cyclomatic_complexity",
94 | "value": 10,
95 | "level": "ignore"
96 | },
97 | "newlines_after_classes": {
98 | "name": "newlines_after_classes",
99 | "value": 3,
100 | "level": "ignore"
101 | },
102 | "no_unnecessary_fat_arrows": {
103 | "name": "no_unnecessary_fat_arrows",
104 | "level": "warn"
105 | },
106 | "missing_fat_arrows": {
107 | "name": "missing_fat_arrows",
108 | "level": "ignore"
109 | },
110 | "non_empty_constructor_needs_parens": {
111 | "name": "non_empty_constructor_needs_parens",
112 | "level": "ignore"
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | let commands = null
4 | let editorObserver = null
5 | let tabsToSpaces = null
6 |
7 | export function activate () {
8 | commands = atom.commands.add('atom-workspace', {
9 | 'tabs-to-spaces:tabify': () => {
10 | loadModule()
11 | tabsToSpaces.tabify()
12 | },
13 | 'tabs-to-spaces:untabify': () => {
14 | loadModule()
15 | tabsToSpaces.untabify()
16 | },
17 | 'tabs-to-spaces:untabify-all': () => {
18 | loadModule()
19 | tabsToSpaces.untabifyAll()
20 | }
21 | })
22 |
23 | editorObserver = atom.workspace.observeTextEditors((editor) => {
24 | handleEvents(editor)
25 | })
26 | }
27 |
28 | export function deactivate () {
29 | if (editorObserver) {
30 | editorObserver.dispose()
31 | }
32 |
33 | if (commands) {
34 | commands.dispose()
35 | }
36 | }
37 |
38 | function handleEvents (editor) {
39 | editor.getBuffer().onWillSave(() => {
40 | if (editor.getPath() === atom.config.getUserConfigPath()) {
41 | return
42 | }
43 |
44 | let onSave = atom.config.get('tabs-to-spaces.onSave', { scope: editor.getRootScopeDescriptor() })
45 | if (onSave === 'tabify') {
46 | loadModule()
47 | tabsToSpaces.tabify()
48 | } else if (onSave === 'untabify') {
49 | loadModule()
50 | tabsToSpaces.untabify()
51 | }
52 | })
53 | }
54 |
55 | function loadModule () {
56 | if (!tabsToSpaces) {
57 | tabsToSpaces = require('./tabs-to-spaces')
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/tabs-to-spaces.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | /**
4 | * Performs all the tabs-to-spacening and vice versa.
5 | */
6 | class TabsToSpaces {
7 | constructor () {
8 | this.allWhitespace = /[ \t]+/g
9 | this.leadingWhitespace = /^[ \t]+/g
10 | }
11 |
12 | /**
13 | * Public: Converts leading whitespace in `editor` to tabs.
14 | *
15 | * * `editor` The editor in which to convert leading whitespace
16 | */
17 | tabify (editor = atom.workspace.getActiveTextEditor()) {
18 | if (!editor) {
19 | return
20 | }
21 |
22 | this.replaceWhitespaceWithTabs(editor)
23 | }
24 |
25 | /**
26 | * Public: Converts leading whitespace in `editor` to spaces.
27 | *
28 | * * `editor` The editor in which to convert leading whitespace
29 | */
30 | untabify (editor = atom.workspace.getActiveTextEditor()) {
31 | if (!editor) {
32 | return
33 | }
34 |
35 | this.replaceWhitespaceWithSpaces(editor)
36 | }
37 |
38 | /**
39 | * Public: Converts all whitespace in `editor` to spaces.
40 | *
41 | * * `editor` The editor in which to convert all whitespace
42 | */
43 | untabifyAll (editor = atom.workspace.getActiveTextEditor()) {
44 | if (!editor) {
45 | return
46 | }
47 |
48 | this.replaceAllWhitespaceWithSpaces(editor)
49 | }
50 |
51 | /**
52 | * Private: Counts the number of character-widths of `text`.
53 | *
54 | * * `text` {String} text to measure
55 | * * `tabLength` {Number} width of a tab character
56 | *
57 | * Returns {Number} of character widths represented by `text`.
58 | */
59 | countSpaces (text, tabLength) {
60 | let count = 0
61 |
62 | for (let ch of text) {
63 | if (ch === ' ') {
64 | count += 1
65 | } else if (ch === '\t') {
66 | count += tabLength
67 | }
68 | }
69 |
70 | return count
71 | }
72 |
73 | /**
74 | * Private: Multiplies `text` by `count`.
75 | *
76 | * * `text` {String} to multiply
77 | * * `count` {Number} of times to repeat `text`
78 | *
79 | * Returns {String} of `text` repeated `count` times.
80 | */
81 | multiplyText (text, count) {
82 | if (isNaN(count) || count === 0) {
83 | return ''
84 | }
85 |
86 | return Array(count + 1).join(text)
87 | }
88 |
89 | replaceAllWhitespaceWithSpaces (editor) {
90 | editor.transact(() => {
91 | editor.scan(this.allWhitespace, ({matchText, replace}) => {
92 | const count = this.countSpaces(matchText, editor.getTabLength())
93 | replace(this.multiplyText(' ', count))
94 | })
95 | })
96 | }
97 |
98 | replaceWhitespaceWithSpaces (editor) {
99 | const tabLength = editor.getTabLength()
100 | const buffer = editor.getBuffer()
101 |
102 | this.processBufferByLine(buffer, ({line, row}) => {
103 | const match = line.match(this.leadingWhitespace)
104 |
105 | if (match) {
106 | const matchText = match[0]
107 | const count = this.countSpaces(matchText, tabLength)
108 | const replacementText = this.multiplyText(' ', count)
109 |
110 | buffer.setTextInRange([[row, 0], [row, matchText.length]], replacementText)
111 | buffer.groupLastChanges()
112 | }
113 | })
114 | }
115 |
116 | replaceWhitespaceWithTabs (editor) {
117 | const tabLength = editor.getTabLength()
118 | const buffer = editor.getBuffer()
119 |
120 | this.processBufferByLine(buffer, ({line, row}) => {
121 | const match = line.match(this.leadingWhitespace)
122 |
123 | if (match) {
124 | const matchText = match[0]
125 | const count = this.countSpaces(matchText, tabLength)
126 | const tabs = Math.floor(count / tabLength)
127 | const spaces = count % tabLength
128 | const replacementText = this.multiplyText('\t', tabs) + this.multiplyText(' ', spaces)
129 |
130 | buffer.setTextInRange([[row, 0], [row, matchText.length]], replacementText)
131 | buffer.groupLastChanges()
132 | }
133 | })
134 | }
135 |
136 | processBufferByLine (buffer, fn) {
137 | buffer.transact(() => {
138 | for (let row = 0, lineCount = buffer.getLineCount(); row < lineCount; ++row) {
139 | const line = buffer.lineForRow(row)
140 |
141 | fn({ line: line, row: row })
142 | }
143 | })
144 | }
145 | }
146 |
147 | module.exports = new TabsToSpaces()
148 |
--------------------------------------------------------------------------------
/menus/tabs-to-spaces.cson:
--------------------------------------------------------------------------------
1 | 'context-menu':
2 | 'atom-text-editor': [
3 | {
4 | 'label': 'Tabs to spaces'
5 | 'submenu': [
6 | { label: 'Tabify', command: 'tabs-to-spaces:tabify' }
7 | { label: 'Untabify', command: 'tabs-to-spaces:untabify' }
8 | { label: 'Untabify All', command: 'tabs-to-spaces:untabify-all' }
9 | ]
10 | }
11 | ]
12 |
13 | 'menu': [
14 | {
15 | 'label': 'Packages'
16 | 'submenu': [
17 | 'label': 'Tabs to Spaces'
18 | 'submenu': [
19 | { 'label': 'Tabify', 'command': 'tabs-to-spaces:tabify' }
20 | { 'label': 'Untabify', 'command': 'tabs-to-spaces:untabify' }
21 | { 'label': 'Untabify All', 'command': 'tabs-to-spaces:untabify-all' }
22 | ]
23 | ]
24 | }
25 | ]
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tabs-to-spaces",
3 | "author": "Lee Dohm ",
4 | "main": "./lib/index",
5 | "version": "1.0.5",
6 | "description": "Provides the ability to convert between leading tabs and spaces in a document",
7 | "keywords": [
8 | "convert",
9 | "indentation",
10 | "spaces",
11 | "tabs"
12 | ],
13 | "repository": "https://github.com/lee-dohm/tabs-to-spaces",
14 | "license": "MIT",
15 | "engines": {
16 | "atom": ">=1.26.0 <2.0.0"
17 | },
18 | "devDependencies": {
19 | "rimraf": "~2.5",
20 | "temp": "~0.6.0"
21 | },
22 | "configSchema": {
23 | "onSave": {
24 | "type": "string",
25 | "default": "none",
26 | "enum": [
27 | "none",
28 | "tabify",
29 | "untabify"
30 | ],
31 | "description": "Setting this to anything other than `none` can **significantly** impact the time it takes to save large files."
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/spec/spec-helper.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | /**
4 | * Indicates whether `element` has a command named `name`.
5 | *
6 | * * `element` {HTMLElement} on which to search commands
7 | * * `name` {String} name of command to search for
8 | *
9 | * Returns {Boolean} indicating whether the command is present on `element`.
10 | */
11 | export function hasCommand (element, name) {
12 | const commands = atom.commands.findCommands({ target: element })
13 |
14 | for (let command of commands) {
15 | if (command.name === name) {
16 | return true
17 | }
18 | }
19 |
20 | return false
21 | }
22 |
--------------------------------------------------------------------------------
/spec/tabs-to-spaces-spec.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | import fs from 'fs'
4 | import path from 'path'
5 | import temp from 'temp'
6 |
7 | const rimraf = require('rimraf')
8 | const tabsToSpaces = require('../lib/tabs-to-spaces')
9 | const helper = require('./spec-helper')
10 |
11 | describe('Tabs To Spaces', function () {
12 | let directory, editor, filePath, workspaceElement
13 |
14 | beforeEach(function () {
15 | directory = temp.mkdirSync()
16 | atom.project.setPaths([directory])
17 | workspaceElement = atom.views.getView(atom.workspace)
18 | filePath = path.join(directory, 'tabs-to-spaces.txt')
19 |
20 | fs.writeFileSync(filePath, '')
21 | atom.config.set('editor.tabLength', 4)
22 |
23 | waitsForPromise(() => {
24 | return Promise.all([
25 | atom.workspace.open(filePath).then((e) => {
26 | editor = e
27 | }),
28 | atom.packages.activatePackage('tabs-to-spaces'),
29 | atom.packages.activatePackage('language-javascript')
30 | ])
31 | })
32 | })
33 |
34 | afterEach(() => {
35 | rimraf.sync(directory)
36 | })
37 |
38 | describe('activate', function () {
39 | it('creates the commands', function () {
40 | expect(helper.hasCommand(workspaceElement, 'tabs-to-spaces:tabify')).toBeTruthy()
41 | expect(helper.hasCommand(workspaceElement, 'tabs-to-spaces:untabify')).toBeTruthy()
42 | expect(helper.hasCommand(workspaceElement, 'tabs-to-spaces:untabify-all')).toBeTruthy()
43 | })
44 | })
45 |
46 | describe('deactivate', function () {
47 | beforeEach(function () {
48 | atom.packages.deactivatePackage('tabs-to-spaces')
49 | })
50 |
51 | it('destroys the commands', function () {
52 | expect(helper.hasCommand(workspaceElement, 'tabs-to-spaces:tabify')).toBeFalsy()
53 | expect(helper.hasCommand(workspaceElement, 'tabs-to-spaces:untabify')).toBeFalsy()
54 | expect(helper.hasCommand(workspaceElement, 'tabs-to-spaces:untabify-all')).toBeFalsy()
55 | })
56 | })
57 |
58 | describe('tabify', function () {
59 | beforeEach(function () {
60 | editor.setTabLength(3)
61 | })
62 |
63 | it('does not change an empty file', function () {
64 | tabsToSpaces.tabify(editor)
65 | expect(editor.getText()).toBe('')
66 | })
67 |
68 | it('does not change spaces at the end of a line', function () {
69 | editor.setText('foobarbaz ')
70 | tabsToSpaces.tabify(editor)
71 | expect(editor.getText()).toBe('foobarbaz ')
72 | })
73 |
74 | it('does not change spaces in the middle of a line', function () {
75 | editor.setText('foo bar baz')
76 | tabsToSpaces.tabify(editor)
77 | expect(editor.getText()).toBe('foo bar baz')
78 | })
79 |
80 | it('converts one tab worth of spaces to a single tab', function () {
81 | editor.setTabLength(2)
82 | editor.setText(' foo')
83 | tabsToSpaces.tabify(editor)
84 | expect(editor.getText()).toBe('\tfoo')
85 | })
86 |
87 | it('converts almost two tabs worth of spaces to one tab and some spaces', function () {
88 | editor.setTabLength(4)
89 | editor.setText(' foo')
90 | tabsToSpaces.tabify(editor)
91 | expect(editor.getText()).toBe('\t foo')
92 | })
93 |
94 | it('changes multiple lines of leading spaces to tabs', function () {
95 | editor.setTabLength(4)
96 | editor.setText(' foo\n bar')
97 | tabsToSpaces.tabify(editor)
98 | expect(editor.getText()).toBe('\tfoo\n\t bar')
99 | })
100 |
101 | it('leaves successive newlines alone', function () {
102 | editor.setTabLength(2)
103 | editor.setText(' foo\n\n bar\n\n baz\n\n')
104 | tabsToSpaces.tabify(editor)
105 | expect(editor.getText()).toBe('\tfoo\n\n\tbar\n\n\tbaz\n\n')
106 | })
107 |
108 | it('changes mixed spaces and tabs to uniform whitespace', function () {
109 | editor.setTabLength(2)
110 | editor.setText('\t \tfoo\n')
111 | tabsToSpaces.tabify(editor)
112 | expect(editor.getText()).toBe('\t\t foo\n')
113 | })
114 | })
115 |
116 | describe('untabify', function () {
117 | beforeEach(function () {
118 | editor.setTabLength(3)
119 | })
120 |
121 | it('does not change an empty file', function () {
122 | tabsToSpaces.untabify(editor)
123 | expect(editor.getText()).toBe('')
124 | })
125 |
126 | it('does not change tabs at the end of a string', function () {
127 | editor.setText('foobarbaz\t')
128 | tabsToSpaces.untabify(editor)
129 | expect(editor.getText()).toBe('foobarbaz\t')
130 | })
131 |
132 | it('does not change tabs in the middle of a string', function () {
133 | editor.setText('foo\tbar\tbaz')
134 | tabsToSpaces.untabify(editor)
135 | expect(editor.getText()).toBe('foo\tbar\tbaz')
136 | })
137 |
138 | it('changes one tab to the correct number of spaces', function () {
139 | editor.setTabLength(2)
140 | editor.setText('\tfoo')
141 | tabsToSpaces.untabify(editor)
142 | expect(editor.getText()).toBe(' foo')
143 | })
144 |
145 | it('changes two tabs to the correct number of spaces', function () {
146 | editor.setTabLength(2)
147 | editor.setText('\t\tfoo')
148 | tabsToSpaces.untabify(editor)
149 | expect(editor.getText()).toBe(' foo')
150 | })
151 |
152 | it('changes multiple lines of leading whitespace to spaces', function () {
153 | editor.setTabLength(2)
154 | editor.setText('\t\tfoo\n\t\tbar\n\n')
155 | tabsToSpaces.untabify(editor)
156 | expect(editor.getText()).toBe(' foo\n bar\n\n')
157 | })
158 |
159 | it('changes mixed spaces and tabs to uniform whitespace', function () {
160 | editor.setTabLength(2)
161 | editor.setText(' \t foo\n')
162 | tabsToSpaces.untabify(editor)
163 | expect(editor.getText()).toBe(' foo\n')
164 | })
165 | })
166 |
167 | describe('untabify all', function () {
168 | beforeEach(function () {
169 | editor.setTabLength(3)
170 | })
171 |
172 | it('does not change an empty file', function () {
173 | tabsToSpaces.untabifyAll(editor)
174 | expect(editor.getText()).toBe('')
175 | })
176 |
177 | it('does change tabs at the end of a string', function () {
178 | editor.setText('foobarbaz\t')
179 | tabsToSpaces.untabifyAll(editor)
180 | expect(editor.getText()).toBe('foobarbaz ')
181 | })
182 |
183 | it('does change tabs in the middle of a string', function () {
184 | editor.setText('foo\tbar\tbaz')
185 | tabsToSpaces.untabifyAll(editor)
186 | expect(editor.getText()).toBe('foo bar baz')
187 | })
188 |
189 | it('changes one tab to the correct number of spaces', function () {
190 | editor.setTabLength(2)
191 | editor.setText('\tfoo')
192 | tabsToSpaces.untabifyAll(editor)
193 | expect(editor.getText()).toBe(' foo')
194 | })
195 |
196 | it('changes two tabs to the correct number of spaces', function () {
197 | editor.setTabLength(2)
198 | editor.setText('\t\tfoo')
199 | tabsToSpaces.untabifyAll(editor)
200 | expect(editor.getText()).toBe(' foo')
201 | })
202 |
203 | it('changes multiple lines of tabs to spaces', function () {
204 | editor.setTabLength(2)
205 | editor.setText('\t\tfoo\n\t\tbar\n\n')
206 | tabsToSpaces.untabifyAll(editor)
207 | expect(editor.getText()).toBe(' foo\n bar\n\n')
208 | })
209 |
210 | it('changes mixed spaces and tabs to uniform whitespace', function () {
211 | editor.setTabLength(2)
212 | editor.setText(' \t foo\n')
213 | tabsToSpaces.untabifyAll(editor)
214 | expect(editor.getText()).toBe(' foo\n')
215 | })
216 | })
217 |
218 | describe('on save', function () {
219 | beforeEach(function () {
220 | atom.config.set('tabs-to-spaces.onSave', 'untabify')
221 | })
222 |
223 | it('will untabify before an editor save a buffer', async function () {
224 | atom.config.set('tabs-to-spaces.onSave', 'untabify')
225 | editor.setText('\t\tfoo\n\t\tbar\n\n')
226 | await editor.save()
227 | expect(editor.getText()).toBe(' foo\n bar\n\n')
228 | })
229 |
230 | it('will tabify before an editor saves a buffer', async function () {
231 | atom.config.set('tabs-to-spaces.onSave', 'tabify')
232 | editor.setText(' foo\n bar\n\n')
233 | await editor.save()
234 | expect(editor.getText()).toBe('\t\tfoo\n\t\tbar\n\n')
235 | })
236 |
237 | describe('with scope-specific configuration', function () {
238 | beforeEach(function () {
239 | atom.config.set('editor.tabLength', 2, { scope: '.text.plain' })
240 | atom.config.set('tabs-to-spaces.onSave', 'tabify', { scope: '.text.plain' })
241 | const filePath = path.join(directory, 'sample.txt')
242 | fs.writeFileSync(filePath, 'Some text.\n')
243 |
244 | waitsForPromise(() => {
245 | return atom.workspace.open(filePath).then((e) => {
246 | editor = e
247 | })
248 | })
249 |
250 | runs(() => {
251 | buffer = editor.getBuffer()
252 | })
253 | })
254 |
255 | it('respects the overridden configuration', async function () {
256 | editor.setText(' foo\n bar\n\n')
257 | await editor.save()
258 | expect(editor.getText()).toBe('\t\tfoo\n\t\tbar\n\n')
259 | })
260 |
261 | it('does not modify the contents of the user configuration file', async function () {
262 | spyOn(atom.config, 'getUserConfigPath').andReturn(filePath)
263 | spyOn(editor, 'getPath').andReturn(filePath)
264 |
265 | editor.setText(' foo\n bar\n\n')
266 | await editor.save()
267 | expect(editor.getText()).toBe(' foo\n bar\n\n')
268 | })
269 | })
270 | })
271 |
272 | describe('invariants', function () {
273 | beforeEach(function () {
274 | editor.setText(fs.readFileSync(__filename, 'utf8'))
275 | })
276 |
277 | it('does not move the position of the cursor', function () {
278 | editor.setCursorBufferPosition([0, 5])
279 | tabsToSpaces.tabify(editor)
280 | tabsToSpaces.untabify(editor)
281 |
282 | pos = editor.getCursorBufferPosition()
283 | expect(pos.row).toBe(0)
284 | expect(pos.column).toBe(5)
285 | })
286 | })
287 | })
288 |
--------------------------------------------------------------------------------