├── utils.py ├── AlignTab.sublime-settings ├── .github └── workflows │ └── test.yaml ├── Default.sublime-keymap ├── LICENSE.txt ├── Default.sublime-commands ├── wclen.py ├── Context.sublime-menu ├── parser.py ├── hist.py ├── Main.sublime-menu ├── table.py ├── aligntab.py ├── README.md └── aligner.py /utils.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | import os 4 | 5 | 6 | class AlignTabEditSettings(sublime_plugin.WindowCommand): 7 | def run(self, **kwargs): 8 | STP = sublime.packages_path() 9 | STPA = os.path.join(STP, "User", "AlignTab") 10 | if not os.path.exists(STPA): 11 | os.makedirs(STPA) 12 | 13 | self.window.run_command("edit_settings", kwargs) 14 | -------------------------------------------------------------------------------- /AlignTab.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // To make it easier to remember complex patterns, you can save them in a 3 | // dictionary in the settings file. To edit the patterns, launch 4 | // Preferences: AlignTab Settings. Use the name as key and the regex as 5 | // value. define your own patterns 6 | "named_patterns": { 7 | // "eq" : "=/f", 8 | // right hand side could also be an array of inputs 9 | // "ifthen" : ["=/f", "\\?/f", ":/f"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run-tests: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | st-version: [3, 4] 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: SublimeText/UnitTesting/actions/setup@master 15 | with: 16 | sublime-text-version: ${{ matrix.st-version }} 17 | - uses: SublimeText/UnitTesting/actions/run-tests@master 18 | with: 19 | coverage: true 20 | codecov-upload: true 21 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["up"], "command": "align_tab_history", 4 | "args": { 5 | "backwards": true 6 | }, 7 | "context": 8 | [ 9 | { "key": "setting.AlignTabInputPanel", "operator": "equal", "operand": true } 10 | ] 11 | }, 12 | 13 | { 14 | "keys": ["down"], "command": "align_tab_history", 15 | "context": 16 | [ 17 | { "key": "setting.AlignTabInputPanel", "operator": "equal", "operand": true } 18 | ] 19 | }, 20 | 21 | { 22 | "keys": ["escape"], "command": "align_tab_clear_mode", 23 | "context": 24 | [ 25 | { "key": "setting.AlignTabTableMode", "operator": "equal", "operand": true } 26 | ] 27 | } 28 | 29 | ] 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Randy Lai 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, 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, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "AlignTab", 4 | "command": "align_tab" 5 | }, 6 | { 7 | "caption": "AlignTab: Table Mode", 8 | "command": "align_tab", 9 | "args": {"mode" : true} 10 | }, 11 | { 12 | "caption": "AlignTab: Live Preview Mode", 13 | "command": "align_tab", 14 | "args": {"live_preview" : true} 15 | }, 16 | { 17 | "caption": "AlignTab: Exit Table Mode", 18 | "command": "align_tab_clear_mode" 19 | }, 20 | { 21 | "caption": "Preferences: AlignTab Settings", 22 | "command": "edit_settings", 23 | "args": 24 | { 25 | "base_file": "${packages}/AlignTab/AlignTab.sublime-settings", 26 | "default": "{\n\t$0\n}\n" 27 | } 28 | }, 29 | { 30 | "caption": "Preferences: AlignTab Context Menu", 31 | "command": "align_tab_edit_settings", 32 | "args": 33 | { 34 | "user_file": "${packages}/User/AlignTab/Context.sublime-menu", 35 | "base_file": "${packages}/AlignTab/Context.sublime-menu", 36 | "default": "[\n\t$0\n]\n" 37 | } 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /wclen.py: -------------------------------------------------------------------------------- 1 | # import from https://github.com/vkocubinsky/SublimeTableEditor/blob/master/widechar_support.py 2 | wide_char_ranges = [ 3 | # http://en.wikipedia.org/wiki/Han_unification 4 | (0x4E00, 0x9FFF), 5 | (0x3400, 0x4DBF), 6 | (0xF900, 0xFAFF), 7 | (0x2E80, 0x2EFF), 8 | (0x3000, 0x303F), 9 | (0x31C0, 0x31EF), 10 | (0x2FF0, 0x2FFF), 11 | (0x2F00, 0x2FDF), 12 | (0x3200, 0x32FF), 13 | (0x3300, 0x33FF), 14 | (0xF900, 0xFAFF), 15 | (0xFE30, 0xFE4F), 16 | # See http://en.wikipedia.org/wiki/Hiragana 17 | (0x3040, 0x309F), 18 | # See http://en.wikipedia.org/wiki/Katakana 19 | (0x30A0, 0x30FF), 20 | # See http://en.wikipedia.org/wiki/Hangul 21 | (0xAC00, 0xD7AF), 22 | (0x1100, 0x11FF), 23 | (0x3130, 0x318F), 24 | (0x3200, 0x32FF), 25 | (0xA960, 0xA97F), 26 | (0xD7B0, 0xD7FF), 27 | (0XFF00, 0XFFEF), 28 | # See http://en.wikipedia.org/wiki/Kanbun 29 | (0x3190, 0x319F), 30 | # See http://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms 31 | (0xFF00, 0xFFEF), 32 | ] 33 | 34 | 35 | def _in_range(c): 36 | c = ord(c) 37 | return any(r[0] <= c <= r[1] for r in wide_char_ranges) 38 | 39 | 40 | def wclen(s): 41 | return sum(2 if _in_range(c) else 1 for c in s) 42 | -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | {"caption" : "-"}, 3 | { 4 | "id": "aligntab", 5 | "caption": "AlignTab", 6 | "children": [ 7 | { 8 | "caption": "First Equal =", 9 | "command": "align_tab", 10 | "args": {"user_input" : "=/f"} 11 | }, 12 | { 13 | "caption" : "First Colon :", 14 | "command" : "align_tab", 15 | "args" : {"user_input" : ":/f"} 16 | }, 17 | { 18 | "caption": "Fat Arrow =>", 19 | "command": "align_tab", 20 | "args": {"user_input" : "=>"} 21 | }, 22 | { 23 | "caption" : "Ampersands &", 24 | "command" : "align_tab", 25 | "args" : {"user_input" : "&"} 26 | }, 27 | { 28 | "caption" : "Vertical Bars |", 29 | "command" : "align_tab", 30 | "args" : {"user_input" : "\\|"} 31 | }, 32 | { 33 | "caption" : "Spaces", 34 | "command" : "align_tab", 35 | "args" : {"user_input" : "\\s*/l1l0"} 36 | }, 37 | { 38 | "caption": "Exit Table Mode", 39 | "command": "align_tab_clear_mode" 40 | } 41 | ] 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def input_parser(user_input): 5 | m = re.match(r"(.+)/([lcru*()0-9]*)(f[0-9]*)?", user_input) 6 | 7 | if m and (m.group(2) or m.group(3)): 8 | regex = m.group(1) 9 | flag = m.group(2) 10 | f = m.group(3) 11 | else: 12 | # print("No flags!") 13 | return [user_input, [['l', 1]], 0] 14 | 15 | try: 16 | # for flag 17 | rParan = re.compile(r"\(([^())]*)\)\*([0-9]+)") 18 | while True: 19 | if not rParan.search(flag): 20 | break 21 | for r in rParan.finditer(flag): 22 | flag = flag.replace(r.group(0), r.group(1) * int(r.group(2)), 1) 23 | 24 | for r in re.finditer(r"([lcru][0-9]*)\*([0-9]+)", flag): 25 | flag = flag.replace(r.group(0), r.group(1) * int(r.group(2)), 1) 26 | 27 | flag = re.findall(r"[lcru][0-9]*", flag) 28 | flag = list(map(lambda x: [x[0], 1] if len(x) == 1 29 | else [x[0], int(x[1:])], flag)) 30 | flag = flag if flag else [['l', 1]] 31 | 32 | # for f 33 | f = 0 if not f else 1 if len(f) == 1 else int(f[1:]) 34 | except Exception: 35 | [regex, flag, f] = [user_input, [['l', 1]], 0] 36 | 37 | return [regex, flag, f] 38 | -------------------------------------------------------------------------------- /hist.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | 5 | class History: 6 | hist = [] 7 | index = None 8 | 9 | def insert(self, user_input): 10 | if not self.hist or (user_input != self.last() and user_input != "last_regex"): 11 | self.hist.append(user_input) 12 | self.index = None 13 | 14 | def roll(self, backwards=False): 15 | if self.index is None: 16 | self.index = -1 if backwards else 0 17 | else: 18 | self.index += -1 if backwards else 1 19 | 20 | if self.index == len(self.hist) or self.index < -len(self.hist): 21 | self.index = -1 if backwards else 0 22 | 23 | def last(self): 24 | return self.hist[-1] if self.hist else None 25 | 26 | def get(self, index=None): 27 | if not index: 28 | index = self.index 29 | return self.hist[index] if self.hist else None 30 | 31 | def reset_index(self): 32 | self.index = None 33 | 34 | 35 | if 'history' not in globals(): 36 | history = History() 37 | 38 | 39 | class AlignTabHistory(sublime_plugin.TextCommand): 40 | def run(self, edit, backwards=False): 41 | history.roll(backwards) 42 | self.view.erase(edit, sublime.Region(0, self.view.size())) 43 | self.view.insert(edit, 0, history.get()) 44 | 45 | 46 | class AlignTabHistoryListener(sublime_plugin.EventListener): 47 | # restore History index 48 | def on_deactivated(self, view): 49 | if view.settings().get("AlignTabInputPanel"): 50 | history.reset_index() 51 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "mnemonic": "P", 11 | "id": "package-settings", 12 | "children": 13 | [ 14 | { 15 | "caption": "AlignTab", 16 | "children": 17 | [ 18 | { 19 | "caption": "Settings", 20 | "command": "edit_settings", 21 | "args": { 22 | "base_file": "${packages}/AlignTab/AlignTab.sublime-settings" 23 | } 24 | }, 25 | { 26 | "caption": "Context Menu", 27 | "command": "align_tab_edit_settings", 28 | "args": 29 | { 30 | "user_file": "${packages}/User/AlignTab/Context.sublime-menu", 31 | "base_file": "${packages}/AlignTab/Context.sublime-menu", 32 | "default": "[\n\t$0\n]\n" 33 | } 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | 42 | 43 | ] 44 | -------------------------------------------------------------------------------- /table.py: -------------------------------------------------------------------------------- 1 | import sublime_plugin 2 | import threading 3 | 4 | 5 | def toggle_table_mode(view, on=True): 6 | if on: 7 | view.settings().set("AlignTabTableMode", True) 8 | view.set_status("aligntab", "[Table Mode]") 9 | else: 10 | view.settings().erase("AlignTabTableMode") 11 | erase_table_rows(view) 12 | view.set_status("aligntab", "") 13 | 14 | 15 | def get_table_rows(view): 16 | R = view.get_regions("AlignTabTableRegion") 17 | rows = [] 18 | for s in R: 19 | for l in view.lines(s): 20 | rows.append(view.rowcol(l.begin())[0]) 21 | return rows 22 | 23 | 24 | def set_table_rows(view, rows): 25 | R = [] 26 | for row in rows: 27 | R.append(view.line(view.text_point(row, 0))) 28 | 29 | view.add_regions("AlignTabTableRegion", R) 30 | 31 | 32 | def erase_table_rows(view): 33 | view.erase_regions("AlignTabTableRegion") 34 | 35 | 36 | class AlignTabModeController(sublime_plugin.EventListener): 37 | # aligntab thread 38 | thread = None 39 | # register for table mode 40 | registered_actions = ["insert", "left_delete", "right_delete", 41 | "delete_word", "paste", "cut"] 42 | 43 | def on_modified(self, view): 44 | if view.is_scratch() or view.settings().get('is_widget'): 45 | return 46 | if self.table_mode(view): 47 | cmdhist = view.command_history(0) 48 | # print(cmdhist) 49 | if cmdhist[0] not in self.registered_actions: 50 | return 51 | delay = 0.2 52 | if cmdhist[0] == "insert" and cmdhist[1]['characters'].strip() == "": 53 | delay = 1 54 | if self.thread: 55 | self.thread.cancel() 56 | self.thread = threading.Timer( 57 | delay, 58 | lambda: view.run_command("align_tab", 59 | {"user_input": "last_regex", "mode": True}) 60 | ) 61 | self.thread.start() 62 | 63 | def on_text_command(self, view, cmd, args): 64 | if view.is_scratch() or view.settings().get('is_widget'): 65 | return 66 | if self.table_mode(view): 67 | if cmd == "undo": 68 | view.run_command("soft_undo") 69 | return ("soft_undo", None) 70 | return None 71 | 72 | def table_mode(self, view): 73 | return view.settings().has("AlignTabTableMode") 74 | 75 | 76 | class AlignTabClearMode(sublime_plugin.TextCommand): 77 | def run(self, edit): 78 | view = self.view 79 | if view.is_scratch() or view.settings().get('is_widget'): 80 | return 81 | toggle_table_mode(view, False) 82 | 83 | def is_enabled(self): 84 | return self.view.settings().get("AlignTabTableMode", False) 85 | 86 | def is_visible(self): 87 | return self.view.settings().get("AlignTabTableMode", False) 88 | -------------------------------------------------------------------------------- /aligntab.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | from .hist import history 4 | from .table import toggle_table_mode 5 | from .aligner import Aligner 6 | 7 | 8 | def resolve_input(user_input): 9 | if isinstance(user_input, str): 10 | s = sublime.load_settings('AlignTab.sublime-settings') 11 | patterns = s.get('named_patterns', {}) 12 | if user_input == 'last_regex' and history.last(): 13 | user_input = history.last() 14 | if user_input in patterns: 15 | user_input = patterns[user_input] 16 | if isinstance(user_input, str): 17 | user_input = [user_input] 18 | return user_input 19 | 20 | 21 | class AlignTabCommand(sublime_plugin.TextCommand): 22 | def run(self, edit, user_input=None, mode=False, live_preview=False): 23 | view = self.view 24 | if not user_input: 25 | self.aligned = False 26 | history.reset_index() 27 | history.roll(backwards=True) 28 | last = history.get() 29 | v = self.view.window().show_input_panel( 30 | 'Align By RegEx:', last or "", 31 | # On Done 32 | lambda x: self.on_done(x, mode, live_preview), 33 | # On Change 34 | lambda x: self.on_change(x) if live_preview else None, 35 | # On Cancel 36 | lambda: self.on_change(None) if live_preview else None 37 | ) 38 | v.run_command("select_all") 39 | v.settings().set('AlignTabInputPanel', True) 40 | else: 41 | user_input = resolve_input(user_input) 42 | error = [] 43 | for uinput in user_input: 44 | # apply align_tab 45 | aligner = Aligner(view, uinput, mode) 46 | self.aligned = aligner.run(edit) 47 | 48 | if self.aligned: 49 | if mode: 50 | # to allow keybinds/commands for tablemode 51 | history.insert(uinput) 52 | toggle_table_mode(view, True) 53 | else: 54 | sublime.status_message("") 55 | else: 56 | if mode and not aligner.adjacent_lines_match(): 57 | toggle_table_mode(view, False) 58 | else: 59 | error.append(uinput) 60 | if error: 61 | errors = ' '.join(error) 62 | sublime.status_message("[Patterns not Found: " + errors + " ]") 63 | 64 | def on_change(self, user_input): 65 | # Undo the previous change if needed 66 | if self.aligned: 67 | self.view.run_command("soft_undo") 68 | self.aligned = False 69 | if user_input: 70 | self.view.run_command("align_tab", {"user_input": user_input, "live_preview": True}) 71 | 72 | def on_done(self, user_input, mode, live_preview): 73 | history.insert(user_input) 74 | # do not double align when done with live preview mode 75 | if not live_preview: 76 | self.view.run_command("align_tab", {"user_input": user_input, "mode": mode}) 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AlignTab 2 | 3 | 4 | [![test](https://github.com/randy3k/AlignTab/actions/workflows/test.yaml/badge.svg)](https://github.com/randy3k/AlignTab/actions/workflows/test.yaml) 5 | [![codecov](https://codecov.io/gh/randy3k/AlignTab/branch/master/graph/badge.svg)](https://codecov.io/gh/randy3k/AlignTab) 6 | 7 | 8 | 9 | 10 | The most flexible alignment plugin for Sublime Text 3. This plugin is inspired by the excellent VIM plugin, [tabular](https://github.com/godlygeek/tabular). 11 | 12 | ST2 support is deprecated but however, it is still possible to install AlignTab on ST2 via Package Control. 13 | 14 | 15 | ## Features 16 | - Align using regular expression 17 | - Custom spacing, padding and justification. 18 | - Smart detection for alignment if no lines are selected 19 | - Multiple cursors support 20 | - Table mode and Live preview mode 21 | 22 | ## Getting started 23 | 24 | - If you only want simple and quick alignment, the predefined alignment will help. 25 | 26 | 27 | 28 | ## More complicated usage 29 | 30 | - Open `AlignTab` in Command Palette `C+Shift+p` and enter the input in the form of `/