├── .gitignore ├── Default (OSX).sublime-keymap ├── example.gif ├── Default (Linux).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── Main.sublime-menu ├── CHANGES.md ├── LICENSE ├── SortJsImportsCommand.py ├── README.md ├── sort_js_imports.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["alt+f5"], "command": "sort_js_imports"} 3 | ] 4 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/sublime-sort-javascript-imports/HEAD/example.gif -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["alt+f9"], "command": "sort_js_imports"} 3 | ] 4 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["alt+f9"], "command": "sort_js_imports"} 3 | ] 4 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | {"caption": "Sort JavaScript Imports", "command": "sort_js_imports"} 3 | ] 4 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "edit", 4 | "children": [ 5 | {"id": "permute"}, 6 | {"command": "sort_js_imports", "caption": "Sort JavaScript Imports", "mnemonic": "J"} 7 | ] 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 1.1.0 / 2016-07-11 2 | 3 | Module name sorting is now case-insensitive. 4 | 5 | Editor contents are no longer touched if sorting didn't change anything. 6 | 7 | ## 1.0.0 / 2016-05-27 8 | 9 | Initial release. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Jonny Buchanan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /SortJsImportsCommand.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import sublime 5 | import sublime_plugin 6 | 7 | sys.path.append(os.path.dirname(sys.executable)) 8 | 9 | try: 10 | from sort_js_imports import sort_js_imports 11 | except ImportError: 12 | from .sort_js_imports import sort_js_imports 13 | 14 | class SortJsImportsCommand(sublime_plugin.TextCommand): 15 | def run(self, edit): 16 | permute_lines(sort_js_imports, self.view, edit) 17 | 18 | # from Packages/Default/sort.py 19 | 20 | def shrink_wrap_region( view, region ): 21 | a, b = region.begin(), region.end() 22 | 23 | for a in range(a, b): 24 | if not view.substr(a).isspace(): 25 | break 26 | 27 | for b in range(b-1, a, -1): 28 | if not view.substr(b).isspace(): 29 | b += 1 30 | break 31 | 32 | return sublime.Region(a, b) 33 | 34 | def shrinkwrap_and_expand_non_empty_selections_to_entire_line(v): 35 | sw = shrink_wrap_region 36 | regions = [] 37 | 38 | for sel in v.sel(): 39 | if not sel.empty(): 40 | regions.append(v.line(sw(v, v.line(sel)))) 41 | v.sel().subtract(sel) 42 | 43 | for r in regions: 44 | v.sel().add(r) 45 | 46 | def permute_lines(f, v, e): 47 | shrinkwrap_and_expand_non_empty_selections_to_entire_line(v) 48 | 49 | regions = [s for s in v.sel() if not s.empty()] 50 | if not regions: 51 | regions = [sublime.Region(0, v.size())] 52 | 53 | regions.sort(reverse=True) 54 | 55 | for r in regions: 56 | txt = v.substr(r) 57 | lines = txt.splitlines() 58 | sorted_lines = f(lines) 59 | 60 | if sorted_lines != lines: 61 | v.replace(e, r, u"\n".join(sorted_lines)) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sort JavaScript Imports 2 | 3 | Adds a Sort JavaScript Imports command to Sublime Text 2 or 3, which sorts selected lines containing JavaScript `import` statements or `require()` calls by the module path they're importing. 4 | 5 | ## Install via [Package Control](https://packagecontrol.io/) 6 | 7 | `Ctrl-Shift-P`/`Command-Shift-P` → Package Control: Install Package → Sort JavaScript Imports 8 | 9 | ## Install via `git clone` 10 | 11 | Preferences → Browse Packages… → `git clone https://github.com/insin/sublime-sort-javascript-imports.git "Sort JavaScript Imports"` 12 | 13 | ## Usage 14 | 15 | Select lines containing the import statements you want to sort, then use either of: 16 | 17 | - Command palette: `Ctrl-Shift-P`/`Command-Shift-P` → Sort JavaScript Imports 18 | - Default key binding: `Alt-F9` on Linux/Windows or `Alt-F5` on Mac 19 | 20 | Lines will be sorted based on the module path being imported, respecting (and normalising) any blank lines used to divide imports into different categories. 21 | 22 | Any non-import lines in the selection will be moved to the end, separated by a new blank line if necessary. [Let me know](https://github.com/insin/sublime-sort-javascript-imports/issues/new) if there's a preferable way to handle these. 23 | 24 | ## Import ordering 25 | 26 | Where top-level imports and path-based imports are mixed in the same block, they will be ordered as follows: 27 | 28 | 1. Top-level imports 29 | 2. Imports which traverse up out of the current directory, from furthest away to closest 30 | 3. Imports within the current directory 31 | 32 | **Note:** if you're using Webpack aliases or a Babel alises plugin for top-level importing of your app's own code, you might want to put those in a separate block for clarity. 33 | 34 | ## Example 35 | 36 | ![](example.gif) 37 | 38 | ## MIT Licensed 39 | 40 | Unit testing and configuration setup cribbed from [Sort Lines (Numerically)](https://github.com/alimony/sublime-sort-numerically). 41 | -------------------------------------------------------------------------------- /sort_js_imports.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import re 3 | 4 | js_module_re = re.compile(r"""(?:^import\s+|\s+from\s+|require\(\s*)['"]([^'"]+)""") 5 | module_weight_re = re.compile(r'^(\./)?((?:\.\./)*)') 6 | whitespace_re = re.compile(r'^\s*$') 7 | 8 | def is_blank(line): 9 | """Determines if a selected line consists entirely of whitespace.""" 10 | return whitespace_re.match(line) is not None 11 | 12 | def is_import(line): 13 | """Determines if a selected line contains an import statement.""" 14 | return js_module_re.search(line) is not None 15 | 16 | def module_sort_key(line): 17 | """ 18 | Extracts the module name from a JavaScript import line and returns a tuple 19 | with a sorting weight and the module name/path as a sort key. 20 | 21 | The following import styles are supported: 22 | 23 | - ``import 'example'`` at the start of a line 24 | - ``...from 'example'`` anywhere in a line 25 | - ``...require('example')`` anywhere in a line 26 | 27 | The following sorting weights are assigned: 28 | 29 | - imports without any path prefixes: ``float('-inf')`` (sort to top) 30 | - imports starting with ``../``: ``-n`` where ``n`` is the the number of 31 | directories ascended (sort furthest to closest) 32 | - imports starting with ``./``: 0 (sort to bottom) 33 | """ 34 | module_name = js_module_re.search(line).group(1) 35 | local_import, ascend_path = module_weight_re.match(module_name).groups() 36 | module_weight = float('-inf') 37 | if local_import: 38 | module_weight = 0 39 | elif ascend_path: 40 | module_weight = -ascend_path.count('/') 41 | return module_weight, module_name.lower() 42 | 43 | def sort_js_imports(lines): 44 | """ 45 | Sorts JavaScript import lines by module name, respecting blank lines which 46 | are in place to separate groups of imports. 47 | """ 48 | sorted_lines = [] 49 | non_imports = [] 50 | 51 | for blank_lines, group in itertools.groupby(lines, key=is_blank): 52 | if blank_lines: 53 | # Normalise gaps between groups of imports to a single blank line 54 | if not sorted_lines or sorted_lines[-1] != '': 55 | sorted_lines.append('') 56 | else: 57 | lines = list(group) 58 | 59 | # Kick any non-import lines down to the end of the selected region 60 | non_import_indices = [i for i, line in enumerate(lines) if not is_import(line)] 61 | if non_import_indices: 62 | non_imports.extend(reversed([lines.pop(i) for i in reversed(non_import_indices)])) 63 | 64 | sorted_lines.extend(sorted(lines, key=module_sort_key)) 65 | 66 | # Add an extra blank line if there were non-imports in the selection 67 | if non_imports and sorted_lines and sorted_lines[-1] != '': 68 | sorted_lines.append('') 69 | 70 | return sorted_lines + non_imports 71 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | if __name__ == '__main__': 4 | import unittest 5 | 6 | from sort_js_imports import sort_js_imports 7 | 8 | class TestSortJsImports(unittest.TestCase): 9 | def test_import_formats(self): 10 | """ 11 | ``import 'foo'``, ``import bar from 'foo'`` and ``require('foo')`` 12 | are supported. 13 | """ 14 | input_lines = [ 15 | 'import "c"', 16 | 'let b = require("b")', 17 | 'import a from "a"', 18 | ] 19 | expected_output_lines = [ 20 | 'import a from "a"', 21 | 'let b = require("b")', 22 | 'import "c"', 23 | ] 24 | self.assertEqual(sort_js_imports(input_lines), expected_output_lines) 25 | 26 | def test_import_case(self): 27 | """ 28 | Import sorting should be case-insensitve. 29 | """ 30 | input_lines = [ 31 | "import Route from 'react-router/lib/Route'", 32 | "import Router from 'react-router/lib/Router'", 33 | "import IndexRoute from 'react-router/lib/IndexRoute'", 34 | "import hashHistory from 'react-router/lib/hashHistory'", 35 | ] 36 | expected_output_lines = [ 37 | "import hashHistory from 'react-router/lib/hashHistory'", 38 | "import IndexRoute from 'react-router/lib/IndexRoute'", 39 | "import Route from 'react-router/lib/Route'", 40 | "import Router from 'react-router/lib/Router'", 41 | ] 42 | self.assertEqual(sort_js_imports(input_lines), expected_output_lines) 43 | 44 | def test_sort_weightings(self): 45 | """ 46 | Where top-level, parent and local imports are mixed together in the 47 | same block, top-level import sorts to the top, then parent imports 48 | from furthest to closest, then local imports from closest to 49 | furthest. 50 | """ 51 | input_lines = [ 52 | 'import "./local"', 53 | 'import "../oneup"', 54 | 'import "../../twoup"', 55 | 'import "toplevel"', 56 | ] 57 | expected_output_lines = [ 58 | 'import "toplevel"', 59 | 'import "../../twoup"', 60 | 'import "../oneup"', 61 | 'import "./local"', 62 | ] 63 | self.assertEqual(sort_js_imports(input_lines), expected_output_lines) 64 | 65 | def test_import_groups(self): 66 | """ 67 | Imports separated by blank lines are sorted within the groups they 68 | delineate. 69 | """ 70 | input_lines = [ 71 | 'import a from "toplevel-b"', 72 | 'import z from "toplevel-a"', 73 | '', 74 | 'import "../../b"', 75 | 'import "../../a"', 76 | 'import "../b"', 77 | 'import "../a"', 78 | '', 79 | 'import "./b"', 80 | 'import "./a"', 81 | ] 82 | expected_output_lines = [ 83 | 'import z from "toplevel-a"', 84 | 'import a from "toplevel-b"', 85 | '', 86 | 'import "../../a"', 87 | 'import "../../b"', 88 | 'import "../a"', 89 | 'import "../b"', 90 | '', 91 | 'import "./a"', 92 | 'import "./b"', 93 | ] 94 | self.assertEqual(sort_js_imports(input_lines), expected_output_lines) 95 | 96 | def test_non_imports(self): 97 | """ 98 | Non-imports mixed in with imports are moved to a separate block 99 | below the selection. 100 | """ 101 | input_lines = [ 102 | 'import express from "express"', 103 | 'let app = express()', 104 | 'import bodyparser from "bodyparser"', 105 | '', 106 | 'import utils from "./utils"', 107 | 'import {secured, unsecured} from "./api"', 108 | ] 109 | expected_output_lines = [ 110 | 'import bodyparser from "bodyparser"', 111 | 'import express from "express"', 112 | '', 113 | 'import {secured, unsecured} from "./api"', 114 | 'import utils from "./utils"', 115 | '', 116 | 'let app = express()', 117 | ] 118 | self.assertEqual(sort_js_imports(input_lines), expected_output_lines) 119 | 120 | unittest.main(argv=['TestSortJsImports']) 121 | --------------------------------------------------------------------------------