├── .gitignore ├── keymaps └── magic-reflow.cson ├── menus └── magic-reflow.cson ├── package.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── spec └── magic-reflow-spec.js └── lib └── magic-reflow.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /keymaps/magic-reflow.cson: -------------------------------------------------------------------------------- 1 | "atom-workspace atom-text-editor": 2 | "alt-q": "magic-reflow:reflow" 3 | -------------------------------------------------------------------------------- /menus/magic-reflow.cson: -------------------------------------------------------------------------------- 1 | "context-menu": 2 | "atom-text-editor": [ 3 | { 4 | "label": "Reflow Selection Magically" 5 | "command": "magic-reflow:reflow" 6 | } 7 | ] 8 | 9 | "menu": [ 10 | { 11 | "label": "Edit" 12 | "submenu": [ 13 | { 14 | "label": "Reflow Selection Magically" 15 | "command": "magic-reflow:reflow" 16 | } 17 | ] 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magic-reflow", 3 | "main": "./lib/magic-reflow", 4 | "version": "0.3.0", 5 | "description": "Re-flow/re-wrap text in a variety of situations (comments, lists, etc.)", 6 | "keywords": [ 7 | "wrap", 8 | "reflow", 9 | "rewrap", 10 | "hard wrap", 11 | "hard-wrap", 12 | "format" 13 | ], 14 | "activationCommands": { 15 | "atom-text-editor": "magic-reflow:reflow" 16 | }, 17 | "repository": "https://github.com/josh-berry/magic-reflow", 18 | "license": "MIT", 19 | "engines": { 20 | "atom": ">=1.8.0 <2.0.0" 21 | }, 22 | "dependencies": {}, 23 | "devDependencies": {} 24 | } 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 - Handle tab characters gracefully 2 | 3 | Added: 4 | 5 | - Detect and clean up inconsistent use of tab and space characters, according to 6 | editor settings. 7 | - Reflow tab characters according to their proper visual width. 8 | 9 | ## 0.2.0 - Fixes for various list issues 10 | 11 | Fixed: 12 | 13 | - Lots of corner cases related to lists, through an almost-complete rewrite. 14 | 15 | Dropped: 16 | 17 | - Support for paragraphs with leading indentation. The new algorithm 18 | unfortunately has lots of trouble with these, and they're not widely used. 19 | 20 | ## 0.1.0 - First Release 21 | 22 | Features: 23 | 24 | - Wrap within common single-line and multi-line source code comments. 25 | - Wrap bulleted and ordered lists (with or without blank lines between each 26 | item). Recognizes numbered and lettered lists of the forms: 27 | - 1., a., A. 28 | - (2), (b), (B) 29 | - Wrap paragraphs with leading indentation 30 | - Wrap paragraphs with block indentation 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Joshua J. Berry 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 | # Magic Reflow 2 | 3 | Magic Reflow will reflow or rewrap text in a variety of situations, ranging from 4 | paragraphs, to comments, to bulleted and ordered lists (of the variety commonly 5 | seen in Markdown and the like). 6 | 7 | ## About 8 | 9 | It is a replacement for the built-in autoflow package -- it handles many of the 10 | things that autoflow handles, and a lot more: 11 | 12 | - Wrap within common single-line and multi-line source code comments. 13 | - Wrap bulleted and ordered lists (with or without blank lines between each 14 | item). Recognizes numbered and lettered lists of the forms: 15 | - 1., a., A. 16 | - (2), (b), (B) 17 | - Wrap nested lists intelligently. 18 | - Wrap paragraphs with block indentation 19 | 20 | ...and various combinations of all of the above. 21 | 22 | ## How to Use Magic Reflow 23 | 24 | 1. Select the text you want to reflow, or put your cursor in the relevant 25 | paragraph. 26 | 2. Hit Alt-Q. 27 | 28 | Note that if you use one of the popular Emacs-emulation packages, you may have 29 | to add the following to your keymap.cson to make the keybinding work: 30 | 31 | 'atom-workspace atom-text-editor': 32 | 'alt-q': 'magic-reflow:reflow' 33 | 34 | You can also use the "Reflow Selection Magically" context menu option, or the 35 | "Edit > Reflow Selection Magically" menu bar option. 36 | 37 | ## Future Work 38 | 39 | - Support for paragraphs with leading indentation 40 | - Support for ASCII-art tables (a la org-mode) 41 | - Ignore code fences (a la Doxygen comments, markdown, etc.) 42 | -------------------------------------------------------------------------------- /spec/magic-reflow-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import MagicReflow from '../lib/magic-reflow'; 4 | 5 | // Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 6 | // 7 | // To run a specific `it` or `describe` block add an `f` to the front (e.g. 8 | // `fit` or `fdescribe`). Remove the `f` to unfocus the block. 9 | 10 | function test([input, line_vlen, tab_vlen, soft_tab], expected) { 11 | console.log(`BEGIN TEST:\n${input}`); 12 | let actual = MagicReflow.reflow( 13 | input, {line_vlen: line_vlen, tab_vlen: tab_vlen, soft_tab: soft_tab}); 14 | console.log(`TEST EXPECTED:\n${expected}`); 15 | console.log(`TEST ACTUAL:\n${[actual]}`); 16 | expect(actual).toBe(expected); 17 | }; 18 | 19 | function qcheck(what, args, expected) { 20 | it(`returns ${expected} for ${args}`, () => { 21 | let actual = what(...args); 22 | expect(actual).toBe(expected); 23 | }); 24 | } 25 | 26 | function qcheckgen(gen, what, args, expected) { 27 | it(`${what}: returns ${expected} for ${args}`, () => { 28 | console.log(`BEGIN TEST ${what}:`, args); 29 | let actual = Array.from(gen(...args)); 30 | console.log(`ACTUAL: `, actual); 31 | console.log(`EXPECTED:`, [expected]); 32 | expect(actual[0]).toBe(expected); 33 | }); 34 | } 35 | 36 | describe('MagicReflow', () => { 37 | describe('vlen', () => { 38 | let vlen = MagicReflow.vlen; 39 | describe('empty strings', () => { 40 | qcheck(vlen, ['', 0, 8], 0); 41 | qcheck(vlen, ['', 4, 8], 0); 42 | }); 43 | 44 | describe('non-tab chars', () => { 45 | qcheck(vlen, [' ', 0, 4], 2); 46 | qcheck(vlen, ['foo', 0, 4], 3); 47 | }); 48 | 49 | describe('simple tabs', () => { 50 | qcheck(vlen, ['\t', 0, 8], 8); 51 | qcheck(vlen, ['\t', 4, 8], 4); 52 | qcheck(vlen, ['\t', 8, 8], 8); 53 | 54 | qcheck(vlen, ['\t', 0, 4], 4); 55 | qcheck(vlen, ['\t', 4, 4], 4); 56 | }); 57 | 58 | describe('short text followed by tab', () => { 59 | qcheck(vlen, ['foo\t', 0, 8], 8); 60 | qcheck(vlen, ['foo\t', 4, 8], 4); 61 | qcheck(vlen, ['foo\t', 8, 8], 8); 62 | 63 | qcheck(vlen, ['foo\t', 0, 4], 4); 64 | qcheck(vlen, ['foo\t', 4, 4], 4); 65 | }); 66 | 67 | describe('tab in middle', () => { 68 | qcheck(vlen, ['foo\tbar', 0, 8], 8+3); 69 | qcheck(vlen, ['foo\tbar', 4, 8], 4+3); 70 | qcheck(vlen, ['foo\tbar', 8, 8], 8+3); 71 | 72 | qcheck(vlen, ['foo\tbar', 0, 4], 4+3); 73 | qcheck(vlen, ['foo\tbar', 4, 4], 4+3); 74 | }); 75 | 76 | describe('multiple tabs', () => { 77 | qcheck(vlen, ['foo\t\t', 0, 8], 8+8); 78 | qcheck(vlen, ['foo\t\t', 4, 8], 4+8); 79 | qcheck(vlen, ['foo\t\t', 8, 8], 8+8); 80 | 81 | qcheck(vlen, ['foo\t\t', 0, 4], 4+4); 82 | qcheck(vlen, ['foo\t\t', 4, 4], 4+4); 83 | }); 84 | }); 85 | 86 | describe('indent_with_tabs', () => { 87 | let iwt = MagicReflow.indent_with_tabs; 88 | 89 | qcheck(iwt, [0, 4], ''); 90 | qcheck(iwt, [1, 4], ' '); 91 | qcheck(iwt, [4, 4], '\t'); 92 | qcheck(iwt, [5, 4], '\t '); 93 | qcheck(iwt, [8, 4], '\t\t'); 94 | qcheck(iwt, [10, 4], '\t\t '); 95 | }); 96 | 97 | describe('leading_tabs_to_spaces', () => { 98 | let gen = MagicReflow.leading_tabs_to_spaces; 99 | 100 | qcheckgen(gen, 'tab', [['\tfoo'], 4], ' foo'); 101 | qcheckgen(gen, 'tab+spc', [['\t foo'], 4], ' foo'); 102 | qcheckgen(gen, 'spc+tab+spc', [[' \t foo'], 4], ' foo'); 103 | qcheckgen(gen, 'spc+tab+spc+tab', [[' \t \tfoo'], 4], ' foo'); 104 | }); 105 | 106 | describe('leading_spaces_to_tabs', () => { 107 | let gen = MagicReflow.leading_spaces_to_tabs; 108 | 109 | qcheckgen(gen, 'tab', [[' foo'], 4], '\tfoo'); 110 | qcheckgen(gen, 'tab+spc', [[' foo'], 4], '\t foo'); 111 | qcheckgen(gen, 'spc+tab+spc', [[' foo'], 4], '\t foo'); 112 | qcheckgen(gen, 'spc+tab+spc+tab', [[' foo'], 4], '\t\tfoo'); 113 | }); 114 | 115 | describe('when reflowing a single paragraph', () => { 116 | it('leaves short lines alone', () => test( 117 | ['This is a short line.'], 118 | 'This is a short line.' 119 | )); 120 | 121 | it('wraps long lines', () => test( 122 | ['This is a much longer line, with more text.', 30], 123 | 'This is a much longer line,\nwith more text.' 124 | )); 125 | 126 | it('leaves leading whitespace intact', () => test( 127 | ['\nLeading whitespace.', 80], 128 | '\nLeading whitespace.' 129 | )); 130 | 131 | it('leaves more leading whitespace intact', () => test( 132 | ['\n\nLeading whitespace.', 80], 133 | '\n\nLeading whitespace.' 134 | )); 135 | 136 | it('leaves trailing whitespace intact', () => test( 137 | ['Trailing whitespace.\n', 80], 138 | 'Trailing whitespace.\n' 139 | )); 140 | 141 | it('leaves more trailing whitespace intact', () => test( 142 | ['Trailing whitespace.\n\n', 80], 143 | 'Trailing whitespace.\n\n' 144 | )); 145 | 146 | it('lets long words overflow', () => test( 147 | ['This IsAReallyLongWordThatDoesntFit in a single line.', 20], 148 | 'This\nIsAReallyLongWordThatDoesntFit\nin a single line.' 149 | )); 150 | 151 | it('re-wraps multiple short lines', () => test( 152 | ['This is a bunch\nof short lines,\nwith some text.', 30], 153 | 'This is a bunch of short\nlines, with some text.' 154 | )); 155 | 156 | it('re-wraps multiple long lines', () => test( 157 | ['This is a long line, followed by another long\nline. This is another long line.', 30], 158 | 'This is a long line, followed\nby another long line. This is\nanother long line.' 159 | )); 160 | 161 | it('puts two spaces after a period at the end of a line', () => test( 162 | ['This is a long line, followed by another long line.\nThis is another long line.', 30], 163 | 'This is a long line, followed\nby another long line. This is\nanother long line.' 164 | )); 165 | 166 | it('leaves leading and trailing blank lines alone', () => test( 167 | ['This is a short line with some words.', 12], 168 | 'This is a\nshort line\nwith some\nwords.' 169 | )); 170 | }); 171 | 172 | describe('when reflowing multiple paragraphs', () => { 173 | it('wraps each paragraph independently', () => test( 174 | ['First paragraph is this.\n\nSecond paragraph is this.', 16], 175 | 'First paragraph\nis this.\n\nSecond paragraph\nis this.' 176 | )); 177 | it('leaves leading and trailing blank lines alone', () => test( 178 | ['\n\nThis is a short line.\n\nAnother paragraph.\n\n', 12], 179 | '\n\nThis is a\nshort line.\n\nAnother\nparagraph.\n\n' 180 | )); 181 | it('re-wraps short lines within a paragraph', () => test( 182 | ['First\nparagraph is this.\n\nSecond\nparagraph is this.', 16], 183 | 'First paragraph\nis this.\n\nSecond paragraph\nis this.' 184 | )); 185 | }); 186 | 187 | // XXX not implemented in the new version 188 | xdescribe('when dealing with leading space characters', () => { 189 | it('wraps a single line in block style', () => test( 190 | [' This line should be wrapped in block style.', 12], 191 | ' This line\n should be\n wrapped\n in block\n style.' 192 | )); 193 | 194 | it('recognizes differing first-line indentation', () => test( 195 | [' This is the first line.\nThis is the second line.', 12], 196 | ' This is\nthe first\nline. This\nis the\nsecond line.' 197 | )); 198 | 199 | it('recognizes block-style indentation', () => test( 200 | [' This is the first line.\n This is the second line.', 14], 201 | ' This is the\n first line.\n This is the\n second line.' 202 | )); 203 | 204 | it('recognizes combined first-line and block-style indentation', () => test( 205 | [' This is the first line.\n This is the second line.', 14], 206 | ' This is\n the first\n line. This\n is the\n second line.' 207 | )); 208 | 209 | it('converts spaces to tabs if appropriate', () => test( 210 | [' This is the first line.', 24, 4, false], 211 | '\tThis is the first line.' 212 | )); 213 | 214 | it('handles inconsistent use of tabs and spaces', () => test( 215 | ['\tLeading tab.\n Second line.', 40, 8, true], 216 | ' Leading tab. Second line.' 217 | )); 218 | 219 | 220 | it('fixes inconsistent indentation', () => test( 221 | [` 222 | This is the first line. 223 | This is the second line. 224 | This is the third line. 225 | `, 14], 226 | ` 227 | This is 228 | the first 229 | line. This 230 | is the 231 | second line. 232 | This is the 233 | third line. 234 | ` 235 | )); 236 | }); 237 | 238 | describe('when dealing with leading tab characters', () => { 239 | it('preserves block style with tabs (one line)', () => test( 240 | ['\tLeading tab.\n\tSecond line.', 40, 8, false], 241 | '\tLeading tab. Second line.' 242 | )); 243 | it('preserves block style with tabs (multi-line)', () => test( 244 | ['\tLeading tab that is a long line.\n\tSecond line.', 24, 8, false], 245 | '\tLeading tab that\n\tis a long line.\n\tSecond line.' 246 | )); 247 | it('uses the correct tab width for indentation', () => test( 248 | ['\tLeading tab that is a long line. Should be 24 cols.', 24, 4, false], 249 | '\tLeading tab that is\n\ta long line. Should\n\tbe 24 cols.', 250 | )); 251 | it('handles tabs after leading sigils', () => test( 252 | ['1.\tThis is a numbered list item that needs wrapping.', 24, 4, false], 253 | '1.\tThis is a numbered\n\tlist item that needs\n\twrapping.' 254 | )); 255 | 256 | it('converts tabs to spaces if appropriate', () => test( 257 | ['\tLeading tab.\n\tSecond line.', 40, 8, true], 258 | ' Leading tab. Second line.' 259 | )); 260 | 261 | it('handles inconsistent use of tabs and spaces', () => test( 262 | ['\tLeading tab.\n Second line.', 40, 8, false], 263 | '\tLeading tab. Second line.' 264 | )); 265 | 266 | // BEGIN: Leading indents are not supported currently. 267 | xit('preserves leading indents with tabs (one line)', () => test( 268 | ['\tLeading tab.\nSecond line.', 40], 269 | '\tLeading tab. Second line.' 270 | )); 271 | xit('preserves leading indents with tabs (multi-line)', () => test( 272 | ['\tLeading tab.\nSecond line.', 40], 273 | '\tLeading tab. Second line.' 274 | )); 275 | // END: leading indents 276 | }); 277 | 278 | for (let sigil of ['#', '##', '//', '///', '--', ';', ';;', ';;;']) { 279 | describe(`when dealing with ${sigil} comments`, () => { 280 | it(`wraps one line of ${sigil} comments correctly`, () => test( 281 | [`${sigil} Hello, world! Have another line.`, 24], 282 | `${sigil} Hello, world! Have\n${sigil} another line.` 283 | )); 284 | it(`wraps many lines of ${sigil} comments correctly`, () => test( 285 | [`${sigil} Hello, world!\n${sigil} Have another line.`, 24], 286 | `${sigil} Hello, world! Have\n${sigil} another line.` 287 | )); 288 | it('recognizes leading whitespace (one line)', () => test( 289 | [` ${sigil} Hello, world! Have another.`, 24], 290 | ` ${sigil} Hello, world!\n ${sigil} Have another.` 291 | 292 | )); 293 | it('recognizes leading whitespace (multi-line)', () => test( 294 | [` ${sigil} Hello, world! Have\n ${sigil} another.`, 24], 295 | ` ${sigil} Hello, world!\n ${sigil} Have another.` 296 | 297 | )); 298 | }); 299 | } 300 | 301 | describe('when reflowing paragraphs with only a leading sigil', () => { 302 | for (let sigil of ['-', '+', '*', '