├── .gitignore ├── Main.sublime-menu ├── RustFmt.py ├── RustFmt.sublime-commands ├── RustFmt.sublime-settings ├── difflib.py ├── messages.json ├── messages ├── 0.1.1.md ├── 0.1.10.md ├── 0.1.3.md ├── 0.1.4.md ├── 0.1.5.md ├── 0.1.6.md ├── 0.1.7.md ├── 0.1.8.md ├── 0.1.9.md └── install.md └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/.gitignore 3 | !/.python-version 4 | !/messages 5 | !/*.py 6 | !/*.json 7 | !/*.md 8 | !/*.sublime-* 9 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "preferences", 4 | "children": [ 5 | { 6 | "id": "package-settings", 7 | "children": [ 8 | { 9 | "caption": "RustFmt", 10 | "children": [ 11 | { 12 | "caption": "Settings", 13 | "command": "edit_settings", 14 | "args": { 15 | "base_file": "${packages}/RustFmt/RustFmt.sublime-settings", 16 | "default": "// RustFmt Settings - User\n{\n\t$0\n}\n" 17 | } 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /RustFmt.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | import subprocess as sub 4 | import os 5 | import sys 6 | from . import difflib 7 | 8 | SETTINGS = 'RustFmt.sublime-settings' 9 | DICT_KEY = 'RustFmt' 10 | IS_WINDOWS = os.name == 'nt' 11 | 12 | 13 | def is_rust_view(view): 14 | return view.score_selector(0, 'source.rust') > 0 15 | 16 | 17 | def get_setting(view, key): 18 | global_overrides = view.settings().get(DICT_KEY) 19 | if isinstance(global_overrides, dict) and key in global_overrides: 20 | return global_overrides[key] 21 | return sublime.load_settings(SETTINGS).get(key) 22 | 23 | 24 | def get_env(view): 25 | val = get_setting(view, 'env') 26 | if val is None: 27 | return None 28 | env = os.environ.copy() 29 | env.update(val) 30 | return env 31 | 32 | 33 | # Copied from other plugins, haven't personally tested on Windows 34 | def process_startup_info(): 35 | if not IS_WINDOWS: 36 | return None 37 | startupinfo = sub.STARTUPINFO() 38 | startupinfo.dwFlags |= sub.STARTF_USESHOWWINDOW 39 | startupinfo.wShowWindow = sub.SW_HIDE 40 | return startupinfo 41 | 42 | 43 | def walk_to_root(path): 44 | if path is None: 45 | return 46 | 47 | if os.path.isdir(path): 48 | yield path 49 | 50 | while not os.path.samefile(path, os.path.dirname(path)): 51 | path = os.path.dirname(path) 52 | yield path 53 | 54 | 55 | def config_for_dir(dir): 56 | path = os.path.join(dir, 'rustfmt.toml') 57 | if os.path.exists(path) and os.path.isfile(path): 58 | return path 59 | 60 | hidden_path = os.path.join(dir, '.rustfmt.toml') 61 | if os.path.exists(hidden_path) and os.path.isfile(hidden_path): 62 | return hidden_path 63 | 64 | return None 65 | 66 | 67 | def find_config_path(path): 68 | for dir in walk_to_root(path): 69 | config = config_for_dir(dir) 70 | if config: 71 | return config 72 | 73 | 74 | def guess_cwd(view): 75 | mode = get_setting(view, 'cwd_mode') 76 | 77 | if mode.startswith(':'): 78 | return mode[1:] 79 | 80 | if mode == 'none': 81 | return None 82 | 83 | if mode == 'project_root': 84 | if len(view.window().folders()): 85 | return view.window().folders()[0] 86 | return None 87 | 88 | if mode == 'auto': 89 | if view.file_name(): 90 | return os.path.dirname(view.file_name()) 91 | elif len(view.window().folders()): 92 | return view.window().folders()[0] 93 | 94 | 95 | def merge_into_view(view, edit, new_src): 96 | def subview(start, end): 97 | return view.substr(sublime.Region(start, end)) 98 | diffs = difflib.myers_diffs(subview(0, view.size()), new_src) 99 | difflib.cleanup_efficiency(diffs) 100 | merged_len = 0 101 | for (op_type, patch) in diffs: 102 | patch_len = len(patch) 103 | if op_type == difflib.Ops.EQUAL: 104 | if subview(merged_len, merged_len+patch_len) != patch: 105 | raise Exception("[sublime-rust-fmt] mismatch between diff's source and current content") 106 | merged_len += patch_len 107 | elif op_type == difflib.Ops.INSERT: 108 | view.insert(edit, merged_len, patch) 109 | merged_len += patch_len 110 | elif op_type == difflib.Ops.DELETE: 111 | if subview(merged_len, merged_len+patch_len) != patch: 112 | raise Exception("[sublime-rust-fmt] mismatch between diff's source and current content") 113 | view.erase(edit, sublime.Region(merged_len, merged_len+patch_len)) 114 | 115 | 116 | def run_format(view, input, encoding): 117 | exec = get_setting(view, 'executable') 118 | args = exec if isinstance(exec, list) else [exec] 119 | 120 | if get_setting(view, 'legacy_write_mode_option'): 121 | args += ['--write-mode', 'display'] 122 | 123 | if get_setting(view, 'use_config_path'): 124 | path = view.file_name() or ( 125 | len(view.window().folders()) and view.window().folders()[0] or None 126 | ) 127 | 128 | config = path and find_config_path(path) 129 | if config: 130 | args += ['--config-path', config] 131 | 132 | proc = sub.Popen( 133 | args=args, 134 | stdin=sub.PIPE, 135 | stdout=sub.PIPE, 136 | stderr=sub.PIPE, 137 | startupinfo=process_startup_info(), 138 | universal_newlines=False, 139 | cwd=guess_cwd(view), 140 | env=get_env(view), 141 | ) 142 | 143 | (stdout, stderr) = proc.communicate(input=bytes(input, encoding=encoding)) 144 | (stdout, stderr) = stdout.decode(encoding), stderr.decode(encoding) 145 | 146 | if proc.returncode != 0: 147 | err = sub.CalledProcessError(proc.returncode, args) 148 | 149 | if get_setting(view, 'error_messages'): 150 | msg = str(err) 151 | if len(stderr) > 0: 152 | msg += ':\n' + stderr 153 | # rustfmt stupidly prints error messages to stdout 154 | elif len(stdout) > 0: 155 | msg += ':\n' + stdout 156 | msg += '\nNote: to disable error popups, set the RustFmt setting "error_messages" to false.' 157 | sublime.error_message(msg) 158 | 159 | raise err 160 | 161 | if len(stderr) > 0: 162 | print('[sublime-rust-fmt]:', stderr, file=sys.stderr) 163 | 164 | return stdout 165 | 166 | 167 | def view_encoding(view): 168 | encoding = view.encoding() 169 | return 'UTF-8' if encoding == 'Undefined' else encoding 170 | 171 | 172 | class rust_fmt_format_buffer(sublime_plugin.TextCommand): 173 | def is_enabled(self): 174 | return is_rust_view(self.view) 175 | 176 | def run(self, edit): 177 | view = self.view 178 | content = view.substr(sublime.Region(0, view.size())) 179 | 180 | stdout = run_format( 181 | view=view, 182 | input=content, 183 | encoding=view_encoding(view), 184 | ) 185 | 186 | merge_type = get_setting(view, 'merge_type') 187 | 188 | if merge_type == 'diff': 189 | merge_into_view(view, edit, stdout) 190 | 191 | elif merge_type == 'replace': 192 | position = view.viewport_position() 193 | view.replace(edit, sublime.Region(0, view.size()), stdout) 194 | # Works only on main thread, hence lambda and timer. 195 | restore = lambda: view.set_viewport_position(position, animate=False) 196 | sublime.set_timeout(restore, 0) 197 | 198 | else: 199 | raise Exception('[sublime-rust-fmt] unknown merge_type setting: {}'.format(merge_type)) 200 | 201 | 202 | class rust_fmt_listener(sublime_plugin.EventListener): 203 | def on_pre_save(self, view): 204 | if is_rust_view(view) and get_setting(view, 'format_on_save'): 205 | view.run_command('rust_fmt_format_buffer') 206 | -------------------------------------------------------------------------------- /RustFmt.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "RustFmt: Format Buffer", 4 | "command": "rust_fmt_format_buffer" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /RustFmt.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | /* 3 | Executable with zero or more arguments. 4 | 5 | When modifying this setting, ensure that the executable expects to 6 | communicate over standard input/output. 7 | 8 | For versions of `rustfmt` prior to 0.8.0, use: 9 | 10 | "executable": ["rustfmt", "--write-mode", "display"] 11 | 12 | To tell `rustfmt` to use the 2018 edition globally, use: 13 | 14 | "executable": ["rustfmt", "--edition", "2018"] 15 | 16 | */ 17 | "executable": ["rustfmt"], 18 | 19 | /* 20 | Additional environment variables for the subprocess. Environment is always 21 | inherited from Sublime Text, which generally tries to mimic your shell env. 22 | This is needed only for additional variables and overrides. 23 | */ 24 | "env": null, 25 | 26 | /* 27 | Format current buffer on save. Enabled by default. 28 | 29 | Note that you can format the buffer manually via the "RustFmt: Format Buffer" 30 | command. 31 | */ 32 | "format_on_save": true, 33 | 34 | /* 35 | Compatibility mode for versions of `rustfmt` prior to 0.8.0. Equivalent to: 36 | 37 | "executable": ["rustfmt", "--write-mode", "display"] 38 | */ 39 | "legacy_write_mode_option": false, 40 | 41 | /* 42 | Whether to search for `rustfmt.toml` in the file's directory and its 43 | ancestors. For anonymous buffers, "current directory" will be guessed from 44 | the current window. 45 | */ 46 | "use_config_path": true, 47 | 48 | /* 49 | Determines the CWD of the subprocess. Possible values: 50 | 51 | - "auto" -- try to use the current file's directory; fall back on 52 | the project root, which is assumed to be the first directory in the 53 | current window 54 | 55 | - "project_root" -- use the project root, which is assumed to be the first 56 | directory in the current window 57 | 58 | - "none" -- don't set the CWD 59 | 60 | - ":" -- use hardcoded path; may be useful for project-specific 61 | settings 62 | */ 63 | "cwd_mode": "auto", 64 | 65 | /* 66 | Show errors from `rustfmt` as popups. Enabled by default. 67 | */ 68 | "error_messages": true, 69 | 70 | /* 71 | Determines how to replace the contents of the current buffer with the 72 | formatted output. Possible values: 73 | 74 | - "diff": -- more complicated but better at preserving cursor position 75 | 76 | - "replace": -- simpler but doesn't preserve cursor position 77 | */ 78 | "merge_type": "diff" 79 | } 80 | -------------------------------------------------------------------------------- /difflib.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions for diff, match and patch. 3 | 4 | Computes the difference between two texts to create a patch. 5 | Applies the patch onto another text, allowing for errors. 6 | 7 | Originally found at http://code.google.com/p/google-diff-match-patch/. 8 | Edited for clarity and simplicity by Nelo Mitranim, 2017. 9 | """ 10 | 11 | import re 12 | from collections import namedtuple 13 | 14 | class Ops(object): 15 | EQUAL = 'EQUAL' 16 | INSERT = 'INSERT' 17 | DELETE = 'DELETE' 18 | 19 | Diff = namedtuple('Diff', ['op', 'text']) 20 | 21 | # Cost of an empty edit operation in terms of edit characters. 22 | DIFF_EDIT_COST = 4 23 | 24 | BLANK_LINE_END = re.compile(r"\n\r?\n$") 25 | 26 | BLANK_LINE_START = re.compile(r"^\r?\n\r?\n") 27 | 28 | def myers_diffs(text1, text2, checklines=True): 29 | """Find the differences between two texts. Simplifies the problem by 30 | stripping any common prefix or suffix off the texts before diffing. 31 | 32 | Args: 33 | text1: Old string to be diffed. 34 | text2: New string to be diffed. 35 | checklines: Optional speedup flag. If present and false, then don't run 36 | a line-level diff first to identify the changed areas. 37 | Defaults to true, which does a faster, slightly less optimal diff. 38 | 39 | Returns: 40 | List of changes. 41 | """ 42 | if text1 == None or text2 == None: 43 | raise ValueError('Null inputs (myers_diffs)') 44 | 45 | # Check for equality (speedup). 46 | if text1 == text2: 47 | if text1: 48 | return [Diff(Ops.EQUAL, text1)] 49 | return [] 50 | 51 | # Trim off common prefix (speedup). 52 | common_length = common_prefix_length(text1, text2) 53 | common_prefix = text1[:common_length] 54 | text1 = text1[common_length:] 55 | text2 = text2[common_length:] 56 | 57 | # Trim off common suffix (speedup). 58 | common_length = common_suffix_length(text1, text2) 59 | if common_length == 0: 60 | commonsuffix = '' 61 | else: 62 | commonsuffix = text1[-common_length:] 63 | text1 = text1[:-common_length] 64 | text2 = text2[:-common_length] 65 | 66 | # Compute the diff on the middle block. 67 | diffs = compute_diffs(text1, text2, checklines) 68 | 69 | # Restore the prefix and suffix. 70 | if common_prefix: 71 | diffs[:0] = [Diff(Ops.EQUAL, common_prefix)] 72 | if commonsuffix: 73 | diffs.append(Diff(Ops.EQUAL, commonsuffix)) 74 | cleanup_merge(diffs) 75 | return diffs 76 | 77 | def compute_diffs(text1, text2, checklines): 78 | """Find the differences between two texts. Assumes that the texts do not 79 | have any common prefix or suffix. 80 | 81 | Args: 82 | text1: Old string to be diffed. 83 | text2: New string to be diffed. 84 | checklines: Speedup flag. If false, then don't run a line-level diff 85 | first to identify the changed areas. 86 | If true, then run a faster, slightly less optimal diff. 87 | 88 | Returns: 89 | List of changes. 90 | """ 91 | if not text1: 92 | # Just add some text (speedup). 93 | return [Diff(Ops.INSERT, text2)] 94 | 95 | if not text2: 96 | # Just delete some text (speedup). 97 | return [Diff(Ops.DELETE, text1)] 98 | 99 | if len(text1) > len(text2): 100 | (longtext, shorttext) = (text1, text2) 101 | else: 102 | (shorttext, longtext) = (text1, text2) 103 | i = longtext.find(shorttext) 104 | if i != -1: 105 | # Shorter text is inside the longer text (speedup). 106 | diffs = [Diff(Ops.INSERT, longtext[:i]), Diff(Ops.EQUAL, shorttext), 107 | Diff(Ops.INSERT, longtext[i + len(shorttext):])] 108 | # Swap insertions for deletions if diff is reversed. 109 | if len(text1) > len(text2): 110 | diffs[0] = diffs[0]._replace(op=Ops.DELETE) 111 | diffs[2] = diffs[2]._replace(op=Ops.DELETE) 112 | return diffs 113 | 114 | if len(shorttext) == 1: 115 | # Single character string. 116 | # After the previous speedup, the character can't be an equality. 117 | return [Diff(Ops.DELETE, text1), Diff(Ops.INSERT, text2)] 118 | 119 | if checklines and len(text1) > 100 and len(text2) > 100: 120 | return line_mode_diffs(text1, text2) 121 | 122 | return diff_bisect(text1, text2) 123 | 124 | def line_mode_diffs(text1, text2): 125 | """Do a quick line-level diff on both strings, then rediff the parts for 126 | greater accuracy. 127 | This speedup can produce non-minimal diffs. 128 | 129 | Args: 130 | text1: Old string to be diffed. 131 | text2: New string to be diffed. 132 | 133 | Returns: 134 | List of changes. 135 | """ 136 | 137 | # Scan the text on a line-by-line basis first. 138 | (text1, text2, line_list) = lines_to_chars(text1, text2) 139 | 140 | diffs = myers_diffs(text1, text2, False) 141 | 142 | # Convert the diff back to original text. 143 | diffs = [diff._replace(text=''.join(line_list[ord(char)] for char in diff.text)) for diff in diffs] 144 | 145 | # Eliminate freak matches (e.g. blank lines) 146 | cleanup_semantic(diffs) 147 | 148 | # Rediff any replacement blocks, this time character-by-character. 149 | # Add a dummy entry at the end. 150 | diffs.append(Diff(Ops.EQUAL, '')) 151 | pointer = 0 152 | count_delete = 0 153 | count_insert = 0 154 | text_delete = '' 155 | text_insert = '' 156 | while pointer < len(diffs): 157 | if diffs[pointer].op == Ops.INSERT: 158 | count_insert += 1 159 | text_insert += diffs[pointer].text 160 | elif diffs[pointer].op == Ops.DELETE: 161 | count_delete += 1 162 | text_delete += diffs[pointer].text 163 | elif diffs[pointer].op == Ops.EQUAL: 164 | # Upon reaching an equality, check for prior redundancies. 165 | if count_delete >= 1 and count_insert >= 1: 166 | # Delete the offending records and add the merged ones. 167 | a = myers_diffs(text_delete, text_insert, False) 168 | diffs[pointer - count_delete - count_insert : pointer] = a 169 | pointer = pointer - count_delete - count_insert + len(a) 170 | count_insert = 0 171 | count_delete = 0 172 | text_delete = '' 173 | text_insert = '' 174 | 175 | pointer += 1 176 | 177 | diffs.pop() # Remove the dummy entry at the end. 178 | 179 | return diffs 180 | 181 | def diff_bisect(text1, text2): 182 | """Find the 'middle snake' of a diff, split the problem in two 183 | and return the recursively constructed diff. 184 | See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. 185 | 186 | Args: 187 | text1: Old string to be diffed. 188 | text2: New string to be diffed. 189 | 190 | Returns: 191 | List of diff tuples. 192 | """ 193 | 194 | # Cache the text lengths to prevent multiple calls. 195 | text1_length = len(text1) 196 | text2_length = len(text2) 197 | max_d = (text1_length + text2_length + 1) // 2 198 | v_offset = max_d 199 | v_length = 2 * max_d 200 | v1 = [-1] * v_length 201 | v1[v_offset + 1] = 0 202 | v2 = v1[:] 203 | delta = text1_length - text2_length 204 | # If the total number of characters is odd, then the front path will 205 | # collide with the reverse path. 206 | front = (delta % 2 != 0) 207 | # Offsets for start and end of k loop. 208 | # Prevents mapping of space beyond the grid. 209 | k1start = 0 210 | k1end = 0 211 | k2start = 0 212 | k2end = 0 213 | for d in range(max_d): 214 | # Walk the front path one step. 215 | for k1 in range(-d + k1start, d + 1 - k1end, 2): 216 | k1_offset = v_offset + k1 217 | if k1 == -d or (k1 != d and 218 | v1[k1_offset - 1] < v1[k1_offset + 1]): 219 | x1 = v1[k1_offset + 1] 220 | else: 221 | x1 = v1[k1_offset - 1] + 1 222 | y1 = x1 - k1 223 | while (x1 < text1_length and y1 < text2_length and 224 | text1[x1] == text2[y1]): 225 | x1 += 1 226 | y1 += 1 227 | v1[k1_offset] = x1 228 | if x1 > text1_length: 229 | # Ran off the right of the graph. 230 | k1end += 2 231 | elif y1 > text2_length: 232 | # Ran off the bottom of the graph. 233 | k1start += 2 234 | elif front: 235 | k2_offset = v_offset + delta - k1 236 | if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] != -1: 237 | # Mirror x2 onto top-left coordinate system. 238 | x2 = text1_length - v2[k2_offset] 239 | if x1 >= x2: 240 | # Overlap detected. 241 | return bisect_split_diffs(text1, text2, x1, y1) 242 | 243 | # Walk the reverse path one step. 244 | for k2 in range(-d + k2start, d + 1 - k2end, 2): 245 | k2_offset = v_offset + k2 246 | if k2 == -d or (k2 != d and 247 | v2[k2_offset - 1] < v2[k2_offset + 1]): 248 | x2 = v2[k2_offset + 1] 249 | else: 250 | x2 = v2[k2_offset - 1] + 1 251 | y2 = x2 - k2 252 | while (x2 < text1_length and y2 < text2_length and 253 | text1[-x2 - 1] == text2[-y2 - 1]): 254 | x2 += 1 255 | y2 += 1 256 | v2[k2_offset] = x2 257 | if x2 > text1_length: 258 | # Ran off the left of the graph. 259 | k2end += 2 260 | elif y2 > text2_length: 261 | # Ran off the top of the graph. 262 | k2start += 2 263 | elif not front: 264 | k1_offset = v_offset + delta - k2 265 | if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] != -1: 266 | x1 = v1[k1_offset] 267 | y1 = v_offset + x1 - k1_offset 268 | # Mirror x2 onto top-left coordinate system. 269 | x2 = text1_length - x2 270 | if x1 >= x2: 271 | # Overlap detected. 272 | return bisect_split_diffs(text1, text2, x1, y1) 273 | 274 | # Number of diffs equals number of characters, no commonality at all. 275 | return [Diff(Ops.DELETE, text1), Diff(Ops.INSERT, text2)] 276 | 277 | def bisect_split_diffs(text1, text2, x, y): 278 | """Given the location of the 'middle snake', split the diff in two parts 279 | and recurse. 280 | 281 | Args: 282 | text1: Old string to be diffed. 283 | text2: New string to be diffed. 284 | x: Index of split point in text1. 285 | y: Index of split point in text2. 286 | 287 | Returns: 288 | List of diff tuples. 289 | """ 290 | text1a = text1[:x] 291 | text2a = text2[:y] 292 | text1b = text1[x:] 293 | text2b = text2[y:] 294 | 295 | # Compute both diffs serially. 296 | diffs = myers_diffs(text1a, text2a, False) 297 | diffsb = myers_diffs(text1b, text2b, False) 298 | 299 | return diffs + diffsb 300 | 301 | def lines_to_chars(text1, text2): 302 | """Split two texts into a list of strings. Reduce the texts to a string 303 | of dicts where each Unicode character represents one line. 304 | 305 | Args: 306 | text1: First string. 307 | text2: Second string. 308 | 309 | Returns: 310 | Three element tuple, containing the encoded text1, the encoded text2 and 311 | the list of unique strings. The zeroth element of the list of unique 312 | strings is intentionally blank. 313 | """ 314 | line_list = [] # e.g. line_list[4] == "Hello\n" 315 | line_dict = {} # e.g. line_dict["Hello\n"] == 4 316 | 317 | # "\x00" is a valid character, but various debuggers don't like it. 318 | # So we'll insert a junk entry to avoid generating a null character. 319 | line_list.append('') 320 | 321 | def lines_to_chars_munge(text): 322 | """Split a text into a list of strings. Reduce the texts to a string 323 | of dicts where each Unicode character represents one line. 324 | Modifies line_list and lineHash through being a closure. 325 | 326 | Args: 327 | text: String to encode. 328 | 329 | Returns: 330 | Encoded string. 331 | """ 332 | chars = [] 333 | # Walk the text, pulling out a substring for each line. 334 | # text.split('\n') would would temporarily double our memory footprint. 335 | # Modifying text would create many large strings to garbage collect. 336 | line_start = 0 337 | line_end = -1 338 | while line_end < len(text) - 1: 339 | line_end = text.find('\n', line_start) 340 | if line_end == -1: 341 | line_end = len(text) - 1 342 | line = text[line_start:line_end + 1] 343 | line_start = line_end + 1 344 | 345 | if line in line_dict: 346 | chars.append(chr(line_dict[line])) 347 | else: 348 | line_list.append(line) 349 | line_dict[line] = len(line_list) - 1 350 | chars.append(chr(len(line_list) - 1)) 351 | return ''.join(chars) 352 | 353 | chars1 = lines_to_chars_munge(text1) 354 | chars2 = lines_to_chars_munge(text2) 355 | return (chars1, chars2, line_list) 356 | 357 | def common_prefix_length(text1, text2): 358 | """Determine the common prefix of two strings. 359 | 360 | Args: 361 | text1: First string. 362 | text2: Second string. 363 | 364 | Returns: 365 | The number of characters common to the start of each string. 366 | """ 367 | # Quick check for common null cases. 368 | if not text1 or not text2 or text1[0] != text2[0]: 369 | return 0 370 | # Binary search. 371 | # Performance analysis: http://neil.fraser.name/news/2007/10/09/ 372 | pointermin = 0 373 | pointermax = min(len(text1), len(text2)) 374 | pointermid = pointermax 375 | pointerstart = 0 376 | while pointermin < pointermid: 377 | if text1[pointerstart:pointermid] == text2[pointerstart:pointermid]: 378 | pointermin = pointermid 379 | pointerstart = pointermin 380 | else: 381 | pointermax = pointermid 382 | pointermid = (pointermax - pointermin) // 2 + pointermin 383 | return pointermid 384 | 385 | def common_suffix_length(text1, text2): 386 | """Determine the common suffix of two strings. 387 | 388 | Args: 389 | text1: First string. 390 | text2: Second string. 391 | 392 | Returns: 393 | The number of characters common to the end of each string. 394 | """ 395 | # Quick check for common null cases. 396 | if not text1 or not text2 or text1[-1] != text2[-1]: 397 | return 0 398 | # Binary search. 399 | # Performance analysis: http://neil.fraser.name/news/2007/10/09/ 400 | pointermin = 0 401 | pointermax = min(len(text1), len(text2)) 402 | pointermid = pointermax 403 | pointerend = 0 404 | while pointermin < pointermid: 405 | if (text1[-pointermid:len(text1) - pointerend] == 406 | text2[-pointermid:len(text2) - pointerend]): 407 | pointermin = pointermid 408 | pointerend = pointermin 409 | else: 410 | pointermax = pointermid 411 | pointermid = (pointermax - pointermin) // 2 + pointermin 412 | return pointermid 413 | 414 | def common_overlap(text1, text2): 415 | """Determine if the suffix of one string is the prefix of another. 416 | 417 | Args: 418 | text1 First string. 419 | text2 Second string. 420 | 421 | Returns: 422 | The number of characters common to the end of the first 423 | string and the start of the second string. 424 | """ 425 | # Cache the text lengths to prevent multiple calls. 426 | text1_length = len(text1) 427 | text2_length = len(text2) 428 | # Eliminate the null case. 429 | if text1_length == 0 or text2_length == 0: 430 | return 0 431 | # Truncate the longer string. 432 | if text1_length > text2_length: 433 | text1 = text1[-text2_length:] 434 | elif text1_length < text2_length: 435 | text2 = text2[:text1_length] 436 | text_length = min(text1_length, text2_length) 437 | # Quick check for the worst case. 438 | if text1 == text2: 439 | return text_length 440 | 441 | # Start by looking for a single character match 442 | # and increase length until no match is found. 443 | # Performance analysis: http://neil.fraser.name/news/2010/11/04/ 444 | best = 0 445 | length = 1 446 | while True: 447 | pattern = text1[-length:] 448 | found = text2.find(pattern) 449 | if found == -1: 450 | return best 451 | length += found 452 | if found == 0 or text1[-length:] == text2[:length]: 453 | best = length 454 | length += 1 455 | 456 | def cleanup_semantic(diffs): 457 | """Reduce the number of edits by eliminating semantically trivial 458 | equalities. 459 | 460 | Args: 461 | diffs: List of diff tuples. 462 | """ 463 | changes = False 464 | equalities = [] # Stack of indices where equalities are found. 465 | lastequality = None # Always equal to diffs[equalities[-1]].text 466 | pointer = 0 # Index of current position. 467 | # Number of chars that changed prior to the equality. 468 | (length_insertions1, length_deletions1) = (0, 0) 469 | # Number of chars that changed after the equality. 470 | (length_insertions2, length_deletions2) = (0, 0) 471 | while pointer < len(diffs): 472 | if diffs[pointer].op == Ops.EQUAL: # Equality found. 473 | equalities.append(pointer) 474 | (length_insertions1, length_insertions2) = (length_insertions2, 0) 475 | (length_deletions1, length_deletions2) = (length_deletions2, 0) 476 | lastequality = diffs[pointer].text 477 | else: # An insertion or deletion. 478 | if diffs[pointer].op == Ops.INSERT: 479 | length_insertions2 += len(diffs[pointer].text) 480 | else: 481 | length_deletions2 += len(diffs[pointer].text) 482 | # Eliminate an equality that is smaller or equal to the edits on both 483 | # sides of it. 484 | if (lastequality and (len(lastequality) <= 485 | max(length_insertions1, length_deletions1)) and 486 | (len(lastequality) <= max(length_insertions2, length_deletions2))): 487 | # Duplicate record. 488 | diffs.insert(equalities[-1], Diff(Ops.DELETE, lastequality)) 489 | # Change second copy to insert. 490 | diffs[equalities[-1] + 1] = diffs[equalities[-1] + 1]._replace(op=Ops.INSERT) 491 | # Throw away the equality we just deleted. 492 | equalities.pop() 493 | # Throw away the previous equality (it needs to be reevaluated). 494 | if len(equalities): 495 | equalities.pop() 496 | if len(equalities): 497 | pointer = equalities[-1] 498 | else: 499 | pointer = -1 500 | # Reset the counters. 501 | length_insertions1, length_deletions1 = 0, 0 502 | length_insertions2, length_deletions2 = 0, 0 503 | lastequality = None 504 | changes = True 505 | pointer += 1 506 | 507 | # Normalize the diff. 508 | if changes: 509 | cleanup_merge(diffs) 510 | cleanup_semantic_lossless(diffs) 511 | 512 | # Find any overlaps between deletions and insertions. 513 | # e.g: abcxxxxxxdef 514 | # -> abcxxxdef 515 | # e.g: xxxabcdefxxx 516 | # -> defxxxabc 517 | # Only extract an overlap if it is as big as the edit ahead or behind it. 518 | pointer = 1 519 | while pointer < len(diffs): 520 | if (diffs[pointer - 1].op == Ops.DELETE and 521 | diffs[pointer].op == Ops.INSERT): 522 | deletion = diffs[pointer - 1].text 523 | insertion = diffs[pointer].text 524 | overlap_length1 = common_overlap(deletion, insertion) 525 | overlap_length2 = common_overlap(insertion, deletion) 526 | if overlap_length1 >= overlap_length2: 527 | if (overlap_length1 >= len(deletion) / 2.0 or 528 | overlap_length1 >= len(insertion) / 2.0): 529 | # Overlap found. Insert an equality and trim the surrounding edits. 530 | diffs.insert(pointer, Diff(Ops.EQUAL, insertion[:overlap_length1])) 531 | diffs[pointer - 1] = Diff(Ops.DELETE, deletion[:len(deletion) - overlap_length1]) 532 | diffs[pointer + 1] = Diff(Ops.INSERT, insertion[overlap_length1:]) 533 | pointer += 1 534 | else: 535 | if (overlap_length2 >= len(deletion) / 2.0 or 536 | overlap_length2 >= len(insertion) / 2.0): 537 | # Reverse overlap found. 538 | # Insert an equality and swap and trim the surrounding edits. 539 | diffs.insert(pointer, Diff(Ops.EQUAL, deletion[:overlap_length2])) 540 | diffs[pointer - 1] = Diff(Ops.INSERT, insertion[:len(insertion) - overlap_length2]) 541 | diffs[pointer + 1] = Diff(Ops.DELETE, deletion[overlap_length2:]) 542 | pointer += 1 543 | pointer += 1 544 | pointer += 1 545 | 546 | def cleanup_semantic_lossless(diffs): 547 | """Look for single edits surrounded on both sides by equalities 548 | which can be shifted sideways to align the edit to a word boundary. 549 | e.g: The cat came. -> The cat came. 550 | 551 | Args: 552 | diffs: List of diff tuples. 553 | """ 554 | 555 | def cleanup_semantic_score(one, two): 556 | """Given two strings, compute a score representing whether the 557 | internal boundary falls on logical boundaries. 558 | Scores range from 6 (best) to 0 (worst). 559 | Closure, but does not reference any external variables. 560 | 561 | Args: 562 | one: First string. 563 | two: Second string. 564 | 565 | Returns: 566 | The score. 567 | """ 568 | if not one or not two: 569 | # Edges are the best. 570 | return 6 571 | 572 | # Each port of this function behaves slightly differently due to 573 | # subtle differences in each language's definition of things like 574 | # 'whitespace'. Since this function's purpose is largely cosmetic, 575 | # the choice has been made to use each language's native features 576 | # rather than force total conformity. 577 | char1 = one[-1] 578 | char2 = two[0] 579 | non_alpha_numeric_1 = not char1.isalnum() 580 | non_alpha_numeric_2 = not char2.isalnum() 581 | whitespace1 = non_alpha_numeric_1 and char1.isspace() 582 | whitespace2 = non_alpha_numeric_2 and char2.isspace() 583 | line_break_1 = whitespace1 and (char1 == "\r" or char1 == "\n") 584 | line_break_2 = whitespace2 and (char2 == "\r" or char2 == "\n") 585 | blank_line_1 = line_break_1 and BLANK_LINE_END.search(one) 586 | blank_line_2 = line_break_2 and BLANK_LINE_START.match(two) 587 | 588 | if blank_line_1 or blank_line_2: 589 | # Five points for blank lines. 590 | return 5 591 | elif line_break_1 or line_break_2: 592 | # Four points for line breaks. 593 | return 4 594 | elif non_alpha_numeric_1 and not whitespace1 and whitespace2: 595 | # Three points for end of sentences. 596 | return 3 597 | elif whitespace1 or whitespace2: 598 | # Two points for whitespace. 599 | return 2 600 | elif non_alpha_numeric_1 or non_alpha_numeric_2: 601 | # One point for non-alphanumeric. 602 | return 1 603 | return 0 604 | 605 | pointer = 1 606 | # Intentionally ignore the first and last element (don't need checking). 607 | while pointer < len(diffs) - 1: 608 | if (diffs[pointer - 1].op == Ops.EQUAL and 609 | diffs[pointer + 1].op == Ops.EQUAL): 610 | # This is a single edit surrounded by equalities. 611 | equality1 = diffs[pointer - 1].text 612 | edit = diffs[pointer].text 613 | equality2 = diffs[pointer + 1].text 614 | 615 | # First, shift the edit as far left as possible. 616 | common_offset = common_suffix_length(equality1, edit) 617 | if common_offset: 618 | common_string = edit[-common_offset:] 619 | equality1 = equality1[:-common_offset] 620 | edit = common_string + edit[:-common_offset] 621 | equality2 = common_string + equality2 622 | 623 | # Second, step character by character right, looking for the best fit. 624 | best_equality_1 = equality1 625 | best_edit = edit 626 | best_equality_2 = equality2 627 | best_score = (cleanup_semantic_score(equality1, edit) + cleanup_semantic_score(edit, equality2)) 628 | while edit and equality2 and edit[0] == equality2[0]: 629 | equality1 += edit[0] 630 | edit = edit[1:] + equality2[0] 631 | equality2 = equality2[1:] 632 | score = (cleanup_semantic_score(equality1, edit) + cleanup_semantic_score(edit, equality2)) 633 | # The >= encourages trailing rather than leading whitespace on edits. 634 | if score >= best_score: 635 | best_score = score 636 | best_equality_1 = equality1 637 | best_edit = edit 638 | best_equality_2 = equality2 639 | 640 | if diffs[pointer - 1].text != best_equality_1: 641 | # We have an improvement, save it back to the diff. 642 | if best_equality_1: 643 | diffs[pointer - 1] = diffs[pointer - 1]._replace(text=best_equality_1) 644 | else: 645 | del diffs[pointer - 1] 646 | pointer -= 1 647 | diffs[pointer] = diffs[pointer]._replace(text=best_edit) 648 | if best_equality_2: 649 | diffs[pointer + 1] = diffs[pointer + 1]._replace(text=best_equality_2) 650 | else: 651 | del diffs[pointer + 1] 652 | pointer -= 1 653 | pointer += 1 654 | 655 | def cleanup_efficiency(diffs): 656 | """Reduce the number of edits by eliminating operationally trivial 657 | equalities. 658 | 659 | Args: 660 | diffs: List of diff tuples. 661 | """ 662 | changes = False 663 | equalities = [] # Stack of indices where equalities are found. 664 | lastequality = None # Always equal to diffs[equalities[-1]].text 665 | pointer = 0 # Index of current position. 666 | pre_ins = False # Is there an insertion operation before the last equality. 667 | pre_del = False # Is there a deletion operation before the last equality. 668 | post_ins = False # Is there an insertion operation after the last equality. 669 | post_del = False # Is there a deletion operation after the last equality. 670 | while pointer < len(diffs): 671 | if diffs[pointer].op == Ops.EQUAL: # Equality found. 672 | if (len(diffs[pointer].text) < DIFF_EDIT_COST and 673 | (post_ins or post_del)): 674 | # Candidate found. 675 | equalities.append(pointer) 676 | pre_ins = post_ins 677 | pre_del = post_del 678 | lastequality = diffs[pointer].text 679 | else: 680 | # Not a candidate, and can never become one. 681 | equalities = [] 682 | lastequality = None 683 | 684 | post_ins = post_del = False 685 | else: # An insertion or deletion. 686 | if diffs[pointer].op == Ops.DELETE: 687 | post_del = True 688 | else: 689 | post_ins = True 690 | 691 | # Five types to be split: 692 | # ABXYCD 693 | # AXCD 694 | # ABXC 695 | # AXCD 696 | # ABXC 697 | 698 | if lastequality and ((pre_ins and pre_del and post_ins and post_del) or 699 | ((len(lastequality) < DIFF_EDIT_COST / 2) and 700 | (pre_ins + pre_del + post_ins + post_del) == 3)): 701 | # Duplicate record. 702 | diffs.insert(equalities[-1], Diff(Ops.DELETE, lastequality)) 703 | # Change second copy to insert. 704 | diffs[equalities[-1] + 1] = Diff(Ops.INSERT, diffs[equalities[-1] + 1].text) 705 | equalities.pop() # Throw away the equality we just deleted. 706 | lastequality = None 707 | if pre_ins and pre_del: 708 | # No changes made which could affect previous entry, keep going. 709 | post_ins = post_del = True 710 | equalities = [] 711 | else: 712 | if len(equalities): 713 | equalities.pop() # Throw away the previous equality. 714 | if len(equalities): 715 | pointer = equalities[-1] 716 | else: 717 | pointer = -1 718 | post_ins = post_del = False 719 | changes = True 720 | pointer += 1 721 | 722 | if changes: 723 | cleanup_merge(diffs) 724 | 725 | def cleanup_merge(diffs): 726 | """Reorder and merge like edit sections. Merge equalities. 727 | Any edit section can move as long as it doesn't cross an equality. 728 | 729 | Args: 730 | diffs: List of diff tuples. 731 | """ 732 | diffs.append(Diff(Ops.EQUAL, '')) # Add a dummy entry at the end. 733 | pointer = 0 734 | count_delete = 0 735 | count_insert = 0 736 | text_delete = '' 737 | text_insert = '' 738 | while pointer < len(diffs): 739 | if diffs[pointer].op == Ops.INSERT: 740 | count_insert += 1 741 | text_insert += diffs[pointer].text 742 | pointer += 1 743 | elif diffs[pointer].op == Ops.DELETE: 744 | count_delete += 1 745 | text_delete += diffs[pointer].text 746 | pointer += 1 747 | elif diffs[pointer].op == Ops.EQUAL: 748 | # Upon reaching an equality, check for prior redundancies. 749 | if count_delete + count_insert > 1: 750 | if count_delete != 0 and count_insert != 0: 751 | # Factor out any common prefixies. 752 | common_length = common_prefix_length(text_insert, text_delete) 753 | if common_length != 0: 754 | x = pointer - count_delete - count_insert - 1 755 | if x >= 0 and diffs[x].op == Ops.EQUAL: 756 | diffs[x] = diffs[x]._replace(text=(diffs[x].text + text_insert[:common_length])) 757 | else: 758 | diffs.insert(0, Diff(Ops.EQUAL, text_insert[:common_length])) 759 | pointer += 1 760 | text_insert = text_insert[common_length:] 761 | text_delete = text_delete[common_length:] 762 | # Factor out any common suffixies. 763 | common_length = common_suffix_length(text_insert, text_delete) 764 | if common_length != 0: 765 | diffs[pointer] = diffs[pointer]._replace(text=( 766 | text_insert[-common_length:] + diffs[pointer].text 767 | )) 768 | text_insert = text_insert[:-common_length] 769 | text_delete = text_delete[:-common_length] 770 | # Delete the offending records and add the merged ones. 771 | if count_delete == 0: 772 | diffs[pointer - count_insert : pointer] = [Diff(Ops.INSERT, text_insert)] 773 | elif count_insert == 0: 774 | diffs[pointer - count_delete : pointer] = [Diff(Ops.DELETE, text_delete)] 775 | else: 776 | diffs[pointer - count_delete - count_insert : pointer] = [ 777 | Diff(Ops.DELETE, text_delete), 778 | Diff(Ops.INSERT, text_insert)] 779 | pointer = pointer - count_delete - count_insert + 1 780 | if count_delete != 0: 781 | pointer += 1 782 | if count_insert != 0: 783 | pointer += 1 784 | elif pointer != 0 and diffs[pointer - 1].op == Ops.EQUAL: 785 | # Merge this equality with the previous one. 786 | diffs[pointer - 1] = diffs[pointer - 1]._replace(text=( 787 | diffs[pointer - 1].text + diffs[pointer].text 788 | )) 789 | del diffs[pointer] 790 | else: 791 | pointer += 1 792 | 793 | count_insert = 0 794 | count_delete = 0 795 | text_delete = '' 796 | text_insert = '' 797 | 798 | if diffs[-1].text == '': 799 | diffs.pop() # Remove the dummy entry at the end. 800 | 801 | # Second pass: look for single edits surrounded on both sides by equalities 802 | # which can be shifted sideways to eliminate an equality. 803 | # e.g: ABAC -> ABAC 804 | changes = False 805 | pointer = 1 806 | # Intentionally ignore the first and last element (don't need checking). 807 | while pointer < len(diffs) - 1: 808 | if (diffs[pointer - 1].op == Ops.EQUAL and 809 | diffs[pointer + 1].op == Ops.EQUAL): 810 | # This is a single edit surrounded by equalities. 811 | if diffs[pointer].text.endswith(diffs[pointer - 1].text): 812 | # Shift the edit over the previous equality. 813 | diffs[pointer] = diffs[pointer]._replace(text=( 814 | diffs[pointer - 1].text + diffs[pointer].text[:-len(diffs[pointer - 1].text)] 815 | )) 816 | diffs[pointer + 1] = diffs[pointer + 1]._replace(text=( 817 | diffs[pointer - 1].text + diffs[pointer + 1].text 818 | )) 819 | del diffs[pointer - 1] 820 | changes = True 821 | elif diffs[pointer].text.startswith(diffs[pointer + 1].text): 822 | # Shift the edit over the next equality. 823 | diffs[pointer - 1] = diffs[pointer - 1]._replace(text=( 824 | diffs[pointer - 1].text + diffs[pointer + 1].text 825 | )) 826 | diffs[pointer] = diffs[pointer]._replace(text=( 827 | diffs[pointer].text[len(diffs[pointer + 1].text):] + diffs[pointer + 1].text 828 | )) 829 | del diffs[pointer + 1] 830 | changes = True 831 | pointer += 1 832 | 833 | # If shifts were made, the diff needs reordering and another shift sweep. 834 | if changes: 835 | cleanup_merge(diffs) 836 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.md", 3 | "0.1.1": "messages/0.1.1.md", 4 | "0.1.3": "messages/0.1.3.md", 5 | "0.1.4": "messages/0.1.4.md", 6 | "0.1.5": "messages/0.1.5.md", 7 | "0.1.6": "messages/0.1.6.md", 8 | "0.1.7": "messages/0.1.7.md", 9 | "0.1.8": "messages/0.1.8.md", 10 | "0.1.9": "messages/0.1.9.md", 11 | "0.1.10": "messages/0.1.10.md" 12 | } 13 | -------------------------------------------------------------------------------- /messages/0.1.1.md: -------------------------------------------------------------------------------- 1 | Unicode should be working now. 2 | 3 | I thought Python3 was supposed to be Unicode-compatible. Such naivete. 4 | -------------------------------------------------------------------------------- /messages/0.1.10.md: -------------------------------------------------------------------------------- 1 | Better formatting method! 2 | 3 | This reverts to the method used before `0.1.6`: diff the contents of the view and the formatted output, and modify the view as little as possible. This avoids or minimizes cursor and scroll jumping. 4 | 5 | If you notice any issues, you can revert to the old method: 6 | 7 | ```sublime-settings 8 | "merge_type": "replace" 9 | ``` 10 | 11 | Or better yet, open an issue via https://github.com/mitranim/sublime-rust-fmt/issues and let me know! 12 | -------------------------------------------------------------------------------- /messages/0.1.3.md: -------------------------------------------------------------------------------- 1 | The `executable` setting now accepts either a string (as before) or a list of 2 | command line arguments: 3 | 4 | ```sublime-settings 5 | "executable": ["rustup", "run", "nightly", "rustfmt"] 6 | ``` 7 | -------------------------------------------------------------------------------- /messages/0.1.4.md: -------------------------------------------------------------------------------- 1 | RustFmt now looks for settings in two places: 2 | 3 | * `RustFmt.sublime-settings` (user or default) 4 | * `"RustFmt"` dict in global Sublime settings (possibly project-specific) 5 | 6 | To override settings on a per-project basis, add a `"RustFmt"` entry to the project-specific config: 7 | 8 | ```sublime-settings 9 | "RustFmt": { 10 | "format_on_save": true, 11 | "executable": "rustfmt" 12 | } 13 | ``` 14 | -------------------------------------------------------------------------------- /messages/0.1.5.md: -------------------------------------------------------------------------------- 1 | RustFmt now supports `rustfmt.toml` and `.rustfmt.toml` files. When formatting, 2 | it recursively searches for these files. If a config is found, it's sent as 3 | `--config-path` to `rustfmt`. 4 | 5 | This setting is on by default. To disable: 6 | 7 | ```sublime-settings 8 | "use_config_path": false 9 | ``` 10 | -------------------------------------------------------------------------------- /messages/0.1.6.md: -------------------------------------------------------------------------------- 1 | Changes: 2 | 3 | * support for newer `rustfmt` 4 | * simpler and faster update of the view buffer 5 | 6 | 1. 7 | 8 | Newer `rustfmt` requires `--emit=stdout` instead of `--write-mode=display`. The plugin uses the new arguments by default. If you have an older version and don't want to update, add this to the plugin settings: 9 | 10 | ```sublime-settings 11 | "legacy_write_mode_option": true, 12 | ``` 13 | 14 | 2. 15 | 16 | Finally found a working way to save/restore scroll position in Sublime. Now, the plugin simply replaces the entire view buffer with the output of `rustfmt`, preserving the scroll position, instead of the ridiculously convoluted diffing algorithm used before. Should be more reliable and faster. If there are any regressions, let me know. 17 | -------------------------------------------------------------------------------- /messages/0.1.7.md: -------------------------------------------------------------------------------- 1 | Added CWD detection for the subprocess. 2 | 3 | By default, the subprocess is invoked with the CWD set to the current file's directory, falling back on the project root, which is assumed to be the first folder in the current window. 4 | 5 | Use the `cwd_mode` setting to override this behavior. See the settings file for the available options. 6 | -------------------------------------------------------------------------------- /messages/0.1.8.md: -------------------------------------------------------------------------------- 1 | 1. The plugin no longer forces space-based indentation. 2 | 3 | 2. Errors from `rustfmt` are shown in ST popups. This can be turned off. 4 | -------------------------------------------------------------------------------- /messages/0.1.9.md: -------------------------------------------------------------------------------- 1 | 1. No mandatory subprocess flags. (Potentially breaking.) 2 | 3 | Previously, the plugin always passed some flags to the subprocess: either 4 | `["--emit", "stdout"]` or `["--write-mode", "display"]`. This could interfere 5 | with a custom executable setting. It also seems unnecessary for the current 6 | version of rustfmt (1.0.0 at the time of writing), which uses 7 | stdin/stdout/stderr by default. 8 | 9 | This version has no mandatory flags. Optional flags are controlled by 10 | `legacy_write_mode_option` and `use_config_path`. Should be more flexible and 11 | less conflict-prone. 12 | 13 | This should just work for anyone with a recent version of rustfmt. For older 14 | versions, you may need this setting: 15 | 16 | "executable": ["rustfmt", "--emit", "stdout"] 17 | 18 | Alternatively, raise an issue at https://github.com/Mitranim/sublime-rust-fmt. 19 | 20 | 2. No mandatory hotkeys. 21 | 22 | To avoid potential conflicts, this plugin no longer comes with hotkeys. To 23 | hotkey the format command, add something like this to your `.sublime-keymap`: 24 | 25 | ```sublime-keymap 26 | { 27 | "keys": ["ctrl+super+k"], 28 | "command": "rust_fmt_format_buffer", 29 | "context": [ 30 | { 31 | "key": "selector", 32 | "operator": "equal", 33 | "operand": "source.rust" 34 | } 35 | ] 36 | } 37 | ``` 38 | 39 | 3. Don't ignore rustfmt warnings. 40 | 41 | When rustfmt exits with code 0, it may still print warnings to stderr. 42 | Previously, they were ignored. Now they're printed to Sublime's console. 43 | -------------------------------------------------------------------------------- /messages/install.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Make sure you have the `rustfmt` executable in your $PATH. Install it with Cargo: 4 | 5 | ```sh 6 | cargo install rustfmt 7 | ``` 8 | 9 | If the plugin can't find the executable, open Preferences → Package Settings → 10 | RustFmt → Settings. Run `which rustfmt` and set the resulting path as the 11 | `executable` setting. On my MacOS system, the path looks like this: 12 | 13 | ```sublime-settings 14 | "executable": "/Users/username/.cargo/bin/rustfmt" 15 | ``` 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | RustFmt is a Sublime Text 3 plugin that auto-formats Rust code with [`rustfmt`](https://github.com/rust-lang-nursery/rustfmt) or another executable. 4 | 5 | Unlike `BeautifyRust`, it's fast and works on buffers that have yet not been saved as files. Unlike `RustFormat`, it preserves the buffer scroll position. It also supports `rustfmt.toml`. 6 | 7 | ## Dependencies 8 | 9 | Requires Sublime Text version 3124 or later. 10 | 11 | Requires [`rustfmt`](https://github.com/rust-lang/rustfmt) to be on [PATH](https://en.wikipedia.org/wiki/PATH_(variable)). Installation: 12 | 13 | ```sh 14 | rustup component add rustfmt 15 | ``` 16 | 17 | ## Installation 18 | 19 | ### Package Control 20 | 21 | 1. Get [Package Control](https://packagecontrol.io) 22 | 2. Open command palette: `Shift+Super+P` or `Shift+Ctrl+P` 23 | 3. `Package Control: Install Package` 24 | 4. `RustFmt` 25 | 26 | ### Manual 27 | 28 | Clone the repo: 29 | 30 | ```sh 31 | git clone https://github.com/mitranim/sublime-rust-fmt.git 32 | ``` 33 | 34 | Then symlink it to your Sublime packages directory. Example for MacOS: 35 | 36 | ```sh 37 | mv sublime-rust-fmt RustFmt 38 | cd RustFmt 39 | ln -sf "$(pwd)" "$HOME/Library/Application Support/Sublime Text 3/Packages/" 40 | ``` 41 | 42 | To find the packages directory, use Sublime Text menu → Preferences → Browse Packages. 43 | 44 | ## Usage 45 | 46 | By default, RustFmt will autoformat files before saving. You can trigger it 47 | manually with the `RustFmt: Format Buffer` command in the command palette. 48 | 49 | If the plugin can't find the executable: 50 | 51 | * run `which rustfmt` to get the absolute executable path 52 | * set it as the `executable` setting, see [Settings](#settings) below 53 | 54 | On MacOS, it might end up like this: 55 | 56 | ```sublime-settings 57 | "executable": ["/Users/username/.cargo/bin/rustfmt"] 58 | ``` 59 | 60 | Can pass additional arguments: 61 | 62 | ```sublime-settings 63 | "executable": ["rustup", "run", "nightly", "rustfmt"] 64 | ``` 65 | 66 | ## Settings 67 | 68 | See [`RustFmt.sublime-settings`](RustFmt.sublime-settings) for all available settings. To override them, open: 69 | 70 | ``` 71 | Preferences → Package Settings → RustFmt → Settings 72 | ``` 73 | 74 | RustFmt looks for settings in the following places: 75 | 76 | * `"RustFmt"` dict in general Sublime settings, possibly project-specific 77 | * `RustFmt.sublime-settings`, default or user-created 78 | 79 | The general Sublime settings take priority. To override them on a per-project basis, create a `"RustFmt"` entry: 80 | 81 | ```sublime-settings 82 | "RustFmt": { 83 | "format_on_save": false 84 | }, 85 | ``` 86 | 87 | ## Commands 88 | 89 | In Sublime's command palette: 90 | 91 | * `RustFmt: Format Buffer` 92 | 93 | ## Hotkeys 94 | 95 | To avoid potential conflicts, this plugin does not come with hotkeys. To hotkey 96 | the format command, add something like this to your `.sublime-keymap`: 97 | 98 | ```sublime-keymap 99 | { 100 | "keys": ["ctrl+super+k"], 101 | "command": "rust_fmt_format_buffer", 102 | "context": [{"key": "selector", "operator": "equal", "operand": "source.rust"}] 103 | } 104 | ``` 105 | 106 | ## License 107 | 108 | https://en.wikipedia.org/wiki/WTFPL 109 | --------------------------------------------------------------------------------