├── .gitignore ├── Context.sublime-menu ├── Default.sublime-commands ├── Example.sublime-keymap ├── FileDiffs.sublime-settings ├── LICENSE ├── Main.sublime-menu ├── README.md ├── Side Bar.sublime-menu ├── Tab Context.sublime-menu ├── file_diffs.py ├── package.json ├── preview_1.png ├── preview_2.png └── preview_3.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "-" }, 3 | { "caption": "FileDiffs Menu", "command": "file_diff_menu" }, 4 | { "caption": "Diff Selections", "command": "file_diff_selections" }, 5 | { "caption": "Diff with Clipboard", "command": "file_diff_clipboard" }, 6 | { "caption": "Diff with Saved", "command": "file_diff_saved" }, 7 | { "caption": "Diff with Previous", "command": "file_diff_previous" }, 8 | { "caption": "-" } 9 | ] 10 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "FileDiffs: Menu", 4 | "command": "file_diff_menu" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /Example.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+shift+d"], "command": "file_diff_menu" }, 3 | { "keys": ["ctrl+shift+e"], "command": "file_diff_menu", "args": {"cmd": ["opendiff", "$file1", "$file2"] } } 4 | ] 5 | -------------------------------------------------------------------------------- /FileDiffs.sublime-settings: -------------------------------------------------------------------------------- 1 | // You can set a command like this 2 | // "cmd": ["command", "$file1", "$file2"] 3 | 4 | // You can also include other command line parameters like this 5 | // "cmd": ["command", "-parameter1", "-parameter2", "$file1", "$file2"] 6 | 7 | { 8 | // just uncomment one of the examples 9 | // or write your own command 10 | // NOTE: Copy/paste example below or write your own command in: Package Settings --> FileDiffs --> Settings - User 11 | // This file will be overwritten if the package is updated. 12 | 13 | // The "command" argument is different depending on your system, for instance 14 | // you might need to prefix /usr/local/bin, or use another explicit path. 15 | 16 | // opendiff (FileMerge) 17 | // "cmd": ["opendiff", "$file1", "$file2"] 18 | 19 | // ksdiff (Kaleidoscope) 20 | // "cmd": ["ksdiff", "$file1", "$file2"] 21 | 22 | // "open_in_sublime": false 23 | // twdiff (Textwrangler) 24 | // "cmd": ["twdiff", "$file1", "$file2"] 25 | 26 | // bbdiff (BBEdit) NOTE: Use example below if you receive error. 27 | // "cmd": ["bbdiff", "$file1", "$file2"] 28 | 29 | // bbdiff (BBEdit) 30 | // "cmd" ["/usr/local/bin/bbdiff", "$file1", "$file2"] 31 | 32 | // deltawalker (DeltaWalker) 33 | // "cmd": ["deltawalker", "-nosplash", "$file1", "$file2"] 34 | 35 | // bcomp (Beyond Compare) 36 | // Install Beyond Compare Command Line Tools: 37 | // Beyond Compare > Install Command Line Tools 38 | // "cmd": ["/usr/local/bin/bcomp", "$file1", "$file2"] 39 | 40 | // "trim_trailing_white_space_before_diff": false 41 | // "expand_full_file_name_in_tab": false 42 | // "apply_tempfile_changes_after_diff_tool": false 43 | // "limit": 1000 44 | 45 | // Reverses "diff with clipboard" direction. 46 | // "reverse_clipboard": true, 47 | 48 | // Number of context lines. Defaults to 3. For full context, set it as "full". 49 | // "context_lines": 3, 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Colin T.A. Gray 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of this project. 27 | -------------------------------------------------------------------------------- /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": "FileDiffs", 16 | "children": 17 | [ 18 | { 19 | "command": "edit_settings", 20 | "args": { 21 | "base_file": "${packages}/FileDiffs/FileDiffs.sublime-settings", 22 | "default": "{\n$0\n}\n" 23 | }, 24 | "caption": "Settings" 25 | }, 26 | { "caption": "-" } 27 | ] 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FileDiffs Plugin 2 | ================ 3 | 4 | Shows diffs between the current file, or selection(s) in the current file, and clipboard, another file, or unsaved changes. Can be configured to show diffs in an external diff tool 5 | 6 | ### Preview 7 | 8 | ![Preview](https://github.com/ildarkhasanshin/SublimeFileDiffs/raw/master/preview_1.png) 9 | 10 | ![Preview](https://github.com/ildarkhasanshin/SublimeFileDiffs/raw/master/preview_2.png) 11 | 12 | ![Preview](https://github.com/ildarkhasanshin/SublimeFileDiffs/raw/master/preview_3.png) 13 | 14 | -------------- 15 | 16 | Help! 17 | ----- 18 | 19 | Check the [wiki][] for more tips 20 | 21 | [wiki]: https://github.com/colinta/SublimeFileDiffs/wiki 22 | 23 | Installation 24 | ------------ 25 | 26 | Using Package Control, install `FileDiffs` or clone this repo in your packages folder. 27 | 28 | I recommended you add key bindings for the commands. I've included my preferred bindings below. 29 | Copy them to your key bindings file (⌘⇧,). 30 | 31 | Add External Diff Tool *(optional)* 32 | -------- 33 | 34 | (IMPORTANT: You might need to make a symlink (e.g. in /usr/local/bin) pointing to the command line tool of your external diff tool) 35 | 36 | 1. Preferences > Package Settings > FileDiffs > Settings - Default 37 | 38 | 2. Uncomment one of the examples or write your own command to open external diff tool. 39 | 40 | This command *may* need to be a full path (e.g. `/usr/local/bin/ksdiff`), if the command isn't in your `PATH`. 41 | 42 | It supports: 43 | 44 | - A generic setting `FileDiffs.sublime-settings` which could be overloaded for each parameter in a platform specific configuration `FileDiffs ($platform).sublime-settings` in the `Settings - User` 45 | - Environment variable expansions for `cmd` parameter in the settings 46 | 47 | Commands 48 | -------- 49 | 50 | `file_diff_menu`: Shows a menu to select one of the file_diff commands. If you use the bindings in Example.sublime-keymap, this is bound to `ctrl+shift+d`. 51 | 52 | The rest of the commands do not need to be bound (accessible from the menu): 53 | 54 | `file_diff_clipboard`: Shows the diff of the current file or selection(s) and the clipboard (the clipboard is considered the "new" file unless `reverse` is True) 55 | 56 | `file_diff_selections`: Shows the diff of the first and second selected regions. The file_diff_menu command checks for exactly two regions selected, otherwise it doesn't display this command. 57 | 58 | `file_diff_saved`: Shows the diff of the current file or selection(s) and the saved file. 59 | 60 | `file_diff_file`: Shows the diff of the current file or selection(s) and a file that is in the current project. 61 | 62 | `file_diff_tab`: Shows the diff of the current file or selection(s) and an open file (aka a file that has a tab). 63 | 64 | `file_diff_previous`: Shows the diff of the current file or selection(s) and the previous activated file. If a file is not saved yet, dirty buffer is used instead of reading from disk. 65 | 66 | If FileDiffs has to use temporary files, they are created in your `Data/Packages` folder (rather than system temp folder) due to privacy concerns for portable Sublime Text installations. Temporary files are automatically removed after 15 seconds. 67 | 68 | Key Bindings 69 | ------------ 70 | 71 | Copy these to your user key bindings file. 72 | 73 | 74 | { "keys": ["ctrl+shift+d"], "command": "file_diff_menu" }, 75 | { "keys": ["ctrl+shift+e"], "command": "file_diff_menu", "args": {"cmd": ["opendiff", "$file1", "$file2"] } }, 76 | 77 | 78 | Contributors 79 | ------------ 80 | 81 | Thanks to: 82 | 83 | - **Sebastian Pape** for adding support for using an external diff tool 84 | - **Starli0n** for merging the ST2 and ST3 branches into one branch, 85 | - and for adding the "Diff file with previous" feature 86 | - **dnsmkl** for helping with diffing temporary files 87 | -------------------------------------------------------------------------------- /Side Bar.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "-" }, 3 | { "caption": "FileDiffs Menu", "command": "file_diff_menu" }, 4 | { "caption": "Diff with File in Project…", "command": "file_diff_file" }, 5 | { "caption": "-" } 6 | ] 7 | -------------------------------------------------------------------------------- /Tab Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "-" }, 3 | { "caption": "FileDiffs Menu", "command": "file_diff_menu" }, 4 | { "caption": "Diff tab with Open Tab…", "command": "file_diff_tab" }, 5 | { "caption": "-" } 6 | ] 7 | -------------------------------------------------------------------------------- /file_diffs.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | import os 3 | import re 4 | import sys 5 | 6 | import sublime 7 | import sublime_plugin 8 | import difflib 9 | import tempfile 10 | import subprocess 11 | 12 | from fnmatch import fnmatch 13 | import codecs 14 | 15 | if sublime.platform() == "windows": 16 | from subprocess import Popen 17 | 18 | 19 | class FileDiffMenuCommand(sublime_plugin.TextCommand): 20 | def run(self, edit, cmd=None): 21 | # Individual menu items. 22 | CLIPBOARD = {'text': 'Diff file with Clipboard', 'command' : 'file_diff_clipboard'} 23 | SELECTIONS = {'text': 'Diff Selections', 'command' : 'file_diff_selections'} 24 | SAVED = {'text': 'Diff file with Saved', 'command' : 'file_diff_saved'} 25 | FILE = {'text': u'Diff file with File in Project…', 'command' : 'file_diff_file'} 26 | TAB = {'text': u'Diff file with Open Tab…', 'command' : 'file_diff_tab'} 27 | PREVIOUS = {'text': 'Diff file with Previous Tab', 'command' : 'file_diff_previous'} 28 | 29 | menu_items = [CLIPBOARD, SELECTIONS, SAVED, FILE, TAB, PREVIOUS] 30 | 31 | non_empty_regions = len([region for region in self.view.sel() if not region.empty()]) 32 | 33 | if non_empty_regions != 2: 34 | menu_items.remove(SELECTIONS) 35 | 36 | if non_empty_regions and non_empty_regions != 2: 37 | for item in menu_items: 38 | item['text'] = item['text'].replace('Diff file', 'Diff selection') 39 | 40 | if cmd is not None: 41 | for item in menu_items: 42 | item['text'] += ' (' + os.path.splitext(os.path.basename(cmd[0]))[0] + ')' 43 | 44 | if not (self.view.file_name() and self.view.is_dirty()): 45 | menu_items.remove(SAVED) 46 | 47 | def on_done(index): 48 | if index >= 0: 49 | self.view.run_command(menu_items[index]['command'], {'cmd': cmd}) 50 | 51 | self.view.window().show_quick_panel([item['text'] for item in menu_items], on_done) 52 | 53 | 54 | class FileDiffCommand(sublime_plugin.TextCommand): 55 | def get_setting(self, key, default=None): 56 | settings = sublime.load_settings('FileDiffs.sublime-settings') 57 | os_specific_settings = {} 58 | if sublime.platform() == 'windows': 59 | os_specific_settings = sublime.load_settings('FileDiffs (Windows).sublime-settings') 60 | elif sublime.platform() == 'osx': 61 | os_specific_settings = sublime.load_settings('FileDiffs (OSX).sublime-settings') 62 | else: 63 | os_specific_settings = sublime.load_settings('FileDiffs (Linux).sublime-settings') 64 | return os_specific_settings.get(key, settings.get(key, default)) 65 | 66 | def diff_content(self, view): 67 | content = '' 68 | 69 | for region in view.sel(): 70 | if region.empty(): 71 | continue 72 | content += view.substr(region) 73 | 74 | if not content: 75 | content = view.substr(sublime.Region(0, view.size())) 76 | return content 77 | 78 | def prep_content(self, ab, file_name, default_name): 79 | content = ab.splitlines(True) 80 | if file_name is None: 81 | file_name = default_name 82 | content = [line.replace("\r\n", "\n").replace("\r", "\n") for line in content] 83 | 84 | trim_trailing_white_space_before_diff = self.get_setting('trim_trailing_white_space_before_diff', False) 85 | if trim_trailing_white_space_before_diff: 86 | content = [line.rstrip() for line in content] 87 | 88 | return (content, file_name) 89 | 90 | def run_diff(self, a, b, from_file, to_file, **options): 91 | external_diff_tool = options.get('cmd') 92 | 93 | if options.get('reverse'): 94 | from_file, to_file = to_file, from_file 95 | a, b = b, a 96 | 97 | (from_content, from_file) = self.prep_content(a, from_file, 'from_file') 98 | (to_content, to_file) = self.prep_content(b, to_file, 'to_file') 99 | 100 | context_lines = self.get_setting("context_lines", 3); 101 | if context_lines == "full": 102 | context_lines = sys.maxsize 103 | 104 | diffs = list(difflib.unified_diff(from_content, to_content, from_file, to_file, n=context_lines)) 105 | 106 | if not diffs: 107 | self.view.show_popup('No Difference') 108 | 109 | else: 110 | external_command = external_diff_tool or self.get_setting('cmd') 111 | open_in_sublime = self.get_setting('open_in_sublime', not external_command) 112 | 113 | if external_command: 114 | self.diff_with_external(external_command, a, b, from_file, to_file, **options) 115 | 116 | if open_in_sublime: 117 | # fix diffs 118 | diffs = map(lambda line: (line and line[-1] == "\n") and line or line + "\n", diffs) 119 | self.diff_in_sublime(diffs) 120 | 121 | def diff_with_external(self, external_command, a, b, from_file=None, to_file=None, **options): 122 | try: 123 | from_file_on_disk = self.file_will_be_read_from_disk(from_file) 124 | to_file_on_disk = self.file_will_be_read_from_disk(to_file) 125 | 126 | # If a dirty file is diffed against the copy on disk, we statically set 127 | # `from_file_on_disk` to True; so that the diff becomes "file to temp" 128 | # instead of "temp to temp". 129 | if to_file == from_file + " (Unsaved)": 130 | view = self.view.window().find_open_file(from_file) 131 | if os.path.exists(from_file) and view and view.is_dirty(): 132 | from_file_on_disk = True 133 | 134 | files_to_remove = [] 135 | 136 | if not from_file_on_disk: 137 | tmp_file = tempfile.NamedTemporaryFile(dir = sublime.packages_path(), prefix = "file-diffs-", suffix = ".temp", delete=False) 138 | from_file = tmp_file.name 139 | tmp_file.close() 140 | files_to_remove.append(tmp_file.name) 141 | 142 | with codecs.open(from_file, encoding='utf-8', mode='w+') as tmp_file: 143 | tmp_file.write(a) 144 | 145 | if not to_file_on_disk: 146 | tmp_file = tempfile.NamedTemporaryFile(dir = sublime.packages_path(), prefix = "file-diffs-", suffix = ".temp", delete=False) 147 | to_file = tmp_file.name 148 | tmp_file.close() 149 | files_to_remove.append(tmp_file.name) 150 | 151 | with codecs.open(to_file, encoding='utf-8', mode='w+') as tmp_file: 152 | tmp_file.write(b) 153 | 154 | trim_trailing_white_space_before_diff = self.get_setting('trim_trailing_white_space_before_diff', False) 155 | if trim_trailing_white_space_before_diff: 156 | def trim_trailing_white_space(file_name): 157 | trim_lines = [] 158 | modified = False 159 | with codecs.open(file_name, encoding='utf-8', mode='r') as f: 160 | lines = f.readlines() 161 | lines = [line.replace("\n", '').replace("\r", '') for line in lines] 162 | for line in lines: 163 | trim_line = line.rstrip() 164 | trim_lines.append(trim_line) 165 | if trim_line != line: 166 | modified = True 167 | if modified: 168 | tmp_file = tempfile.NamedTemporaryFile(dir = sublime.packages_path(), prefix = "file-diffs-", suffix = ".temp", delete=False) 169 | file_name = tmp_file.name 170 | tmp_file.close() 171 | files_to_remove.append(tmp_file.name) 172 | with codecs.open(file_name, encoding='utf-8', mode='w+') as f: 173 | f.writelines('\n'.join(trim_lines)) 174 | return file_name 175 | 176 | from_file = trim_trailing_white_space(from_file) 177 | to_file = trim_trailing_white_space(to_file) 178 | 179 | if os.path.exists(from_file): 180 | external_command = [c.replace('$file1', from_file) for c in external_command] 181 | external_command = [c.replace('$file2', to_file) for c in external_command] 182 | external_command = [os.path.expandvars(c) for c in external_command] 183 | if sublime.platform() == "windows": 184 | Popen(external_command) 185 | else: 186 | subprocess.Popen(external_command) 187 | 188 | apply_tempfile_changes_after_diff_tool = self.get_setting('apply_tempfile_changes_after_diff_tool', False) 189 | post_diff_tool = options.get('post_diff_tool') 190 | if apply_tempfile_changes_after_diff_tool and post_diff_tool is not None and (not from_file_on_disk or not to_file_on_disk): 191 | if from_file_on_disk: 192 | from_file = None 193 | if to_file_on_disk: 194 | to_file = None 195 | # Use a dialog to block st and wait for the closing of the diff tool 196 | if sublime.ok_cancel_dialog("Apply changes from tempfile after external diff tool execution?"): 197 | post_diff_tool(from_file, to_file) 198 | except Exception as e: 199 | # some basic logging here, since we are cluttering the /tmp folder 200 | self.view.show_popup(str(e)) 201 | 202 | finally: 203 | def remove_files(): 204 | for file in files_to_remove: 205 | os.remove(file) 206 | 207 | # Remove temp files after 15 seconds. We don't remove immediately, 208 | # because external diff tools may take some time to read them from disk. 209 | sublime.set_timeout_async(remove_files, 15000) 210 | 211 | def diff_in_sublime(self, diffs): 212 | diffs = ''.join(diffs) 213 | scratch = self.view.window().new_file() 214 | scratch.set_scratch(True) 215 | scratch.set_syntax_file('Packages/Diff/Diff.tmLanguage') 216 | scratch.run_command('file_diff_dummy1', {'content': diffs}) 217 | 218 | def read_file(self, file_name): 219 | content = '' 220 | with codecs.open(file_name, mode='U', encoding='utf-8') as f: 221 | content = f.read() 222 | return content 223 | 224 | def get_file_name(self, view, default_name): 225 | file_name = '' 226 | if view.file_name(): 227 | file_name = view.file_name() 228 | elif view.name(): 229 | file_name = view.name() 230 | else: 231 | file_name = default_name 232 | return file_name 233 | 234 | def get_content_from_file(self, file_name): 235 | with codecs.open(file_name, encoding='utf-8', mode='r') as f: 236 | lines = f.readlines() 237 | lines = [line.replace("\r\n", "\n").replace("\r", "\n") for line in lines] 238 | content = ''.join(lines) 239 | return content 240 | 241 | def update_view(self, view, edit, tmp_file): 242 | if tmp_file: 243 | non_empty_regions = [region for region in view.sel() if not region.empty()] 244 | nb_non_empty_regions = len(non_empty_regions) 245 | region = None 246 | if nb_non_empty_regions == 0: 247 | region = sublime.Region(0, view.size()) 248 | elif nb_non_empty_regions == 1: 249 | region = non_empty_regions[0] 250 | else: 251 | self.view.show_popup('Cannot update multiselection') 252 | return 253 | view.replace(edit, region, self.get_content_from_file(tmp_file)) 254 | 255 | def file_will_be_read_from_disk(self, file): 256 | view = self.view.window().find_open_file(file) 257 | return os.path.exists(file) and not (view and view.is_dirty()) and not (view and self.view_has_a_selection(view)) 258 | 259 | def view_has_a_selection(self, view): 260 | """Checks if it has exactly one non-empty selection.""" 261 | return len(view.sel()) == 1 and not view.sel()[0].empty() 262 | 263 | 264 | class FileDiffDummy1Command(sublime_plugin.TextCommand): 265 | def run(self, edit, content): 266 | self.view.insert(edit, 0, content) 267 | 268 | 269 | class FileDiffClipboardCommand(FileDiffCommand): 270 | def run(self, edit, **kwargs): 271 | to_file = self.get_file_name(self.view, 'untitled') 272 | for region in self.view.sel(): 273 | if not region.empty(): 274 | to_file += ' (Selection)' 275 | break 276 | clipboard = sublime.get_clipboard() 277 | def on_post_diff_tool(from_file, to_file): 278 | self.update_view(self.view, edit, to_file) 279 | sublime.set_clipboard(self.get_content_from_file(from_file)) 280 | 281 | reverse = kwargs.get('reverse') or self.get_setting('reverse_clipboard', False) 282 | kwargs.update({'post_diff_tool': on_post_diff_tool, 'reverse': reverse}) 283 | 284 | self.run_diff(clipboard, self.diff_content(self.view), 285 | from_file='(clipboard)', 286 | to_file=to_file, 287 | **kwargs) 288 | 289 | def is_visible(self): 290 | return bool(sublime.get_clipboard()) 291 | 292 | 293 | class FileDiffSelectionsCommand(FileDiffCommand): 294 | def trim_indent(self, lines): 295 | indent = None 296 | for line in lines: 297 | # ignore blank lines 298 | if not line: 299 | continue 300 | 301 | new_indent = re.match('[ \t]*', line).group(0) 302 | # ignore lines that only consist of whitespace 303 | if len(new_indent) == len(line): 304 | continue 305 | 306 | if indent is None: 307 | indent = new_indent 308 | elif len(new_indent) < len(indent): 309 | indent = new_indent 310 | 311 | if not indent: 312 | break 313 | return indent 314 | 315 | def run(self, edit, **kwargs): 316 | regions = self.view.sel() 317 | first_selection = self.view.substr(regions[0]) 318 | second_selection = self.view.substr(regions[1]) 319 | 320 | # trim off indent 321 | indent = self.trim_indent(first_selection.splitlines()) 322 | if indent: 323 | first_selection = "\n".join(line[len(indent):] for line in first_selection.splitlines()) 324 | 325 | # trim off indent 326 | indent = self.trim_indent(second_selection.splitlines()) 327 | if indent: 328 | second_selection = "\n".join(line[len(indent):] for line in second_selection.splitlines()) 329 | 330 | self.run_diff(first_selection, second_selection, 331 | from_file='first selection', 332 | to_file='second selection', 333 | **kwargs) 334 | 335 | def is_visible(self): 336 | return len(self.view.sel()) > 1 337 | 338 | 339 | class FileDiffSavedCommand(FileDiffCommand): 340 | def run(self, edit, **kwargs): 341 | def on_post_diff_tool(from_file, to_file): 342 | self.update_view(self.view, edit, to_file) 343 | 344 | kwargs.update({'post_diff_tool': on_post_diff_tool}) 345 | self.run_diff(self.read_file(self.view.file_name()), self.diff_content(self.view), 346 | from_file=self.view.file_name(), 347 | to_file=self.view.file_name() + ' (Unsaved)', 348 | **kwargs) 349 | 350 | def is_visible(self): 351 | return bool(self.view.file_name()) and self.view.is_dirty() 352 | 353 | 354 | class FileDiffFileCommand(FileDiffCommand): 355 | def run(self, edit, **kwargs): 356 | common = None 357 | folders = self.view.window().folders() 358 | files = self.find_files(folders, []) 359 | for folder in folders: 360 | if common is None: 361 | common = folder 362 | else: 363 | common_len = len(common) 364 | while folder[0:common_len] != common[0:common_len]: 365 | common_len -= 1 366 | common = common[0:common_len] 367 | 368 | my_file = self.view.file_name() 369 | # filter out my_file 370 | files = [f for f in files if f != my_file] 371 | # shorten names using common length 372 | file_picker = [f[len(common):] for f in files] 373 | 374 | def on_done(index): 375 | if index > -1: 376 | self.run_diff(self.diff_content(self.view), self.read_file(files[index]), 377 | from_file=self.view.file_name(), 378 | to_file=files[index], 379 | **kwargs) 380 | sublime.set_timeout(lambda: self.view.window().show_quick_panel(file_picker, on_done), 1) 381 | 382 | def find_files(self, folders, ret=[]): 383 | # Cannot access these settings!! WHY!? 384 | # folder_exclude_patterns = self.view.get_setting('folder_exclude_patterns') 385 | # file_exclude_patterns = self.view.get_setting('file_exclude_patterns') 386 | folder_exclude_patterns = [".svn", ".git", ".hg", "CVS"] 387 | file_exclude_patterns = ["*.pyc", "*.pyo", "*.exe", "*.dll", "*.obj", "*.o", "*.a", "*.lib", "*.so", "*.dylib", "*.ncb", "*.sdf", "*.suo", "*.pdb", "*.idb", ".DS_Store", "*.class", "*.psd", "*.db"] 388 | max_files = self.get_setting('limit', 1000) 389 | 390 | for folder in folders: 391 | if not os.path.isdir(folder): 392 | continue 393 | 394 | for f in os.listdir(folder): 395 | fullpath = os.path.join(folder, f) 396 | if os.path.isdir(fullpath): 397 | # excluded folder? 398 | if not len([True for pattern in folder_exclude_patterns if fnmatch(f, pattern)]): 399 | self.find_files([fullpath], ret) 400 | else: 401 | # excluded file? 402 | if not len([True for pattern in file_exclude_patterns if fnmatch(f, pattern)]): 403 | ret.append(fullpath) 404 | if len(ret) >= max_files: 405 | self.view.show_popup('Too many files to include all of them in this list') 406 | return ret 407 | return ret 408 | 409 | 410 | class FileDiffTabCommand(FileDiffCommand): 411 | def run(self, edit, **kwargs): 412 | my_id = self.view.id() 413 | files = [] 414 | contents = [] 415 | views = [] 416 | untitled_count = 1 417 | for v in self.view.window().views(): 418 | if v.id() != my_id: 419 | this_content = v.substr(sublime.Region(0, v.size())) 420 | if v.file_name(): 421 | files.append(v.file_name()) 422 | elif v.name(): 423 | files.append(v.name()) 424 | else: 425 | files.append('untitled %d' % untitled_count) 426 | untitled_count += 1 427 | 428 | contents.append(this_content) 429 | views.append(v) 430 | 431 | def on_done(index): 432 | if index > -1: 433 | def on_post_diff_tool(from_file, to_file): 434 | self.update_view(self.view, edit, from_file) 435 | self.update_view(views[index], edit, to_file) 436 | 437 | kwargs.update({'post_diff_tool': on_post_diff_tool}) 438 | self.run_diff(self.diff_content(self.view), contents[index], 439 | from_file=self.view.file_name(), 440 | to_file=files[index], 441 | **kwargs) 442 | 443 | if len(files) == 1: 444 | on_done(0) 445 | else: 446 | if self.get_setting('expand_full_file_name_in_tab', False): 447 | menu_items = [[os.path.basename(f),f] for f in files] 448 | else: 449 | menu_items = [os.path.basename(f) for f in files] 450 | sublime.set_timeout(lambda: self.view.window().show_quick_panel(menu_items, on_done), 1) 451 | 452 | def is_visible(self): 453 | return len(self.view.window().views()) > 1 454 | 455 | 456 | previous_view = current_view = None 457 | 458 | class FileDiffPreviousCommand(FileDiffCommand): 459 | def run(self, edit, **kwargs): 460 | if previous_view: 461 | def on_post_diff_tool(from_file, to_file): 462 | self.update_view(previous_view, edit, from_file) 463 | self.update_view(current_view, edit, to_file) 464 | 465 | kwargs.update({'post_diff_tool': on_post_diff_tool}) 466 | self.run_diff(self.diff_content(previous_view), self.diff_content(self.view), 467 | from_file=self.get_file_name(previous_view, 'untitled (Previous)'), 468 | to_file=self.get_file_name(self.view, 'untitled (Current)'), 469 | **kwargs) 470 | 471 | def is_visible(self): 472 | return previous_view is not None 473 | 474 | def record_current_view(view): 475 | global previous_view 476 | global current_view 477 | previous_view = current_view 478 | current_view = view 479 | 480 | class FileDiffListener(sublime_plugin.EventListener): 481 | def on_activated(self, view): 482 | try: 483 | # Prevent 'show_quick_panel()' of 'FileDiffs Menu' from being recorded 484 | viewids = [v.id() for v in view.window().views()] 485 | if view.id() not in viewids: 486 | return 487 | if current_view is None or view.id() != current_view.id(): 488 | record_current_view(view) 489 | except AttributeError: 490 | pass 491 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FileDiffs", 3 | "details": "https://github.com/colinta/SublimeFileDiffs", 4 | "description": "Shows diffs between the current file, or selection(s) in the current file, and clipboard, another file, or unsaved changes. With contributions from Sebastian Pape (spape) and Jiri Urban (jiriurban)", 5 | "author": "Colin T.A. Gray (colinta)", 6 | "labels": [ 7 | "diff/merge" 8 | ], 9 | "releases": [ 10 | { 11 | "sublime_text": ">=3000", 12 | "tags": true 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /preview_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinta/SublimeFileDiffs/cb5215ad31e01aef3029878893542ec1a07d4a3d/preview_1.png -------------------------------------------------------------------------------- /preview_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinta/SublimeFileDiffs/cb5215ad31e01aef3029878893542ec1a07d4a3d/preview_2.png -------------------------------------------------------------------------------- /preview_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinta/SublimeFileDiffs/cb5215ad31e01aef3029878893542ec1a07d4a3d/preview_3.png --------------------------------------------------------------------------------