├── .python-version ├── diffy_lib ├── __init__.py ├── diffy_test.py └── diffier.py ├── Diffy.sublime-settings ├── Default.sublime-commands ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Context.sublime-menu ├── .gitignore ├── LICENSE ├── README.md ├── Main.sublime-menu └── diffy.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /diffy_lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Diffy.sublime-settings: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "Diffy Compare", "command": "diffy" }, 3 | { "caption": "Diffy Cleanup", "command": "diffy", "args": {"action": "clear"} }, 4 | ] 5 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ 4 | "ctrl+k", 5 | "ctrl+d" 6 | ], 7 | "command": "diffy" 8 | }, 9 | { 10 | "keys": [ 11 | "ctrl+k", 12 | "ctrl+c" 13 | ], 14 | "command": "diffy", 15 | "args": { 16 | "action": "clear" 17 | } 18 | } 19 | ] -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ 4 | "super+k", 5 | "super+d" 6 | ], 7 | "command": "diffy" 8 | }, 9 | { 10 | "keys": [ 11 | "super+k", 12 | "super+c" 13 | ], 14 | "command": "diffy", 15 | "args": { 16 | "action": "clear" 17 | } 18 | } 19 | ] -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ 4 | "ctrl+k", 5 | "ctrl+d" 6 | ], 7 | "command": "diffy" 8 | }, 9 | { 10 | "keys": [ 11 | "ctrl+k", 12 | "ctrl+c" 13 | ], 14 | "command": "diffy", 15 | "args": { 16 | "action": "clear" 17 | } 18 | } 19 | ] -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "-", 4 | "id": "separator" 5 | }, 6 | { 7 | "caption": "Diffy (2 Columns)", 8 | "mnemonic": "D", 9 | "id": "diffy", 10 | "children": 11 | [ 12 | { 13 | "id" : "diffyStart", 14 | "caption" : "Compare", 15 | "command" : "diffy" 16 | }, 17 | { 18 | "id" : "diffyEnd", 19 | "caption": "Clear", 20 | "command": "diffy", 21 | "args": {"action": "clear"} 22 | } 23 | ] 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ziang Song 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Diffy 2 | ============================ 3 | 4 | This is a file comparison plugin for both Sublime 2 and 3. 5 | 6 | Enjoy! 7 | 8 | ### Installation 9 | Please install Sublime [Package Control]("https://sublime.wbond.net/installation") first. Then inside *Package Control: Install Package*, type *Diffy* and then click to confirm. 10 | 11 | ### Usage 12 | After installing the plugin, set the layout to be 2 columns via *View -> Layout -> Columns: 2*. And make sure you have files (or temporary files pasted from clipboard) opened side by side. 13 | 14 | 1. To compare and show the diffs, press **CTRL + k** followed by **CTRL + d**. 15 | 2. To clear the marked lines, press press **CTRL + k** followed by **CTRL + c**. 16 | 17 | ### Settings 18 | #### The default key binding for Mac is 19 | 20 | ``` 21 | { "keys": ["super+k", "super+d"], "command": "diffy" } 22 | { "keys": ["super+k", "super+c"], "command": "diffy", "args": {"action": "clear"} } 23 | ``` 24 | 25 | #### The default key binding for Windows / Linux is 26 | 27 | ``` 28 | { "keys": ["ctrl+k", "ctrl+d"], "command": "diffy" } 29 | { "keys": ["ctrl+k", "ctrl+c"], "command": "diffy", "args": {"action": "clear"} } 30 | ``` 31 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mnemonic": "n", 4 | "caption": "Preferences", 5 | "id": "preferences", 6 | "children": [ 7 | { 8 | "mnemonic": "P", 9 | "caption": "Package Settings", 10 | "id": "package-settings", 11 | "children": [ 12 | { 13 | "caption": "Diffy", 14 | "children": [ 15 | { 16 | "caption": "Settings Default", 17 | "args": { 18 | "file": "${packages}/Diffy/Diffy.sublime-settings" 19 | }, 20 | "command": "open_file" 21 | }, 22 | { 23 | "caption": "Settings User", 24 | "args": { 25 | "file": "${packages}/User/Diffy.sublime-settings" 26 | }, 27 | "command": "open_file" 28 | }, 29 | { 30 | "caption": "-" 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /diffy_lib/diffy_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pprint import pprint as pp 3 | import diffier 4 | 5 | text_1 = """ 6 | Using scent: 7 | nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] 8 | test_config (tests.test_common.test_config.Test_Config) ... ok 9 | 10 | ---------------------------------------------------------------------- 11 | Ran 1 test in 0.003s 12 | 13 | OK 14 | asdfasdf 15 | In good standing 16 | """ 17 | 18 | text_2 = """ 19 | Using scent: 20 | nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] 21 | adf 22 | test_config (tests.test_common.test_config.Test_Config) ... ok 23 | 24 | ---------------------------------------------------------------------- 25 | Ran 1 test in 0.003s 26 | 27 | 28 | 29 | OK 30 | asdfasdf 31 | In good standing 32 | """ 33 | 34 | class TestDiffy(unittest.TestCase): 35 | 36 | def setUp(self): 37 | self.diffy = diffier.Diffy() 38 | 39 | def test_calculate_diff(self): 40 | 41 | 42 | result1, result2 = self.diffy.calculate_diff(text_1.split('\n'), text_2.split('\n')) 43 | 44 | self.assertEqual( 45 | [r.get_data() for r in result1], 46 | [(6, 0), (9, 24)] 47 | ) 48 | 49 | self.assertEqual( 50 | [r.get_data() for r in result2], 51 | [(3, 0), (8, 0), (9, 0), (10, 0), (12, 24)] 52 | ) 53 | 54 | if __name__ == '__main__': 55 | unittest.main() -------------------------------------------------------------------------------- /diffy.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | import sys 3 | 4 | if sys.version_info >= (3, 0): 5 | from .diffy_lib import diffier 6 | else: 7 | from diffy_lib import diffier 8 | 9 | class DiffyCommand(sublime_plugin.TextCommand): 10 | def get_entire_content(self, view): 11 | selection = sublime.Region(0, view.size()) 12 | content = view.substr(selection) 13 | return content 14 | 15 | def clear(self, view): 16 | view.erase_regions('highlighted_lines') 17 | 18 | 19 | """ 20 | return the marked lines 21 | """ 22 | def draw_difference(self, view, diffs): 23 | self.clear(view) 24 | 25 | lines = [d.get_region(view) for d in diffs] 26 | 27 | view.add_regions( 28 | 'highlighted_lines', 29 | lines, 30 | 'keyword', 31 | 'dot', 32 | sublime.DRAW_OUTLINED 33 | ) 34 | 35 | return lines 36 | 37 | 38 | def set_view_point(self, view, lines): 39 | if len(lines) > 0: 40 | view.show(lines[0]) 41 | 42 | def run(self, edit, **kwargs): 43 | diffy = diffier.Diffy() 44 | window = self.view.window() 45 | 46 | action = kwargs.get('action', None) 47 | 48 | view_1 = window.selected_sheets()[0].view() if len(window.selected_sheets()) >= 2 else window.active_view_in_group(0) 49 | view_2 = window.selected_sheets()[1].view() if len(window.selected_sheets()) >= 2 else window.active_view_in_group(1) 50 | 51 | if action == 'clear': 52 | if view_1: self.clear(view_1) 53 | if view_2: self.clear(view_2) 54 | else: 55 | #make sure there are 2 columns side by side 56 | if view_1 and view_2: 57 | text_1 = self.get_entire_content(view_1) 58 | text_2 = self.get_entire_content(view_2) 59 | 60 | if len(text_1) > 0 and len(text_2) > 0: 61 | diff_1, diff_2 = diffy.calculate_diff(text_1.split('\n'), text_2.split('\n')) 62 | 63 | highlighted_lines_1 = self.draw_difference(view_1, diff_1) 64 | highlighted_lines_2 = self.draw_difference(view_2, diff_2) 65 | 66 | self.set_view_point(view_1, highlighted_lines_1) 67 | self.set_view_point(view_2, highlighted_lines_2) 68 | -------------------------------------------------------------------------------- /diffy_lib/diffier.py: -------------------------------------------------------------------------------- 1 | 2 | import difflib 3 | from pprint import pprint as pp 4 | 5 | 6 | class RegionToDraw(object): 7 | def __init__(self, line_number, start): 8 | self.line_number = line_number 9 | self.start = start 10 | 11 | def get_data(self): 12 | return (self.line_number, self.start) 13 | 14 | def __str__(self): 15 | return "" 16 | 17 | def __repr__(self): 18 | return self.__str__() 19 | 20 | 21 | class LineToDraw(RegionToDraw): 22 | def __init__(self, line_number, start): 23 | super(LineToDraw, self).__init__(line_number, start) 24 | 25 | def get_region(self, view): 26 | point = view.text_point(self.line_number, 0) 27 | return view.line(point) 28 | 29 | def __str__(self): 30 | return "LineToDraw: {line_number}".format(line_number=self.line_number) 31 | 32 | 33 | class WordToDraw(RegionToDraw): 34 | def __init__(self, line_number, start, end): 35 | super(WordToDraw, self).__init__(line_number, start) 36 | self.end = end 37 | 38 | def get_region(self, view): 39 | point_start = view.text_point(self.line_number, self.start) 40 | point_end = view.text_point(self.line_number, self.end) 41 | 42 | #take advantage of sublime's API to highlight a word 43 | return view.word(point_start) 44 | 45 | def __str__(self): 46 | return "WordToDraw: {line_number}: ({start}, {end})".format(line_number=self.line_number,start=self.start, end=self.end) 47 | 48 | 49 | class Diffy(object): 50 | def parse_diff_list(self, lst): 51 | #add a sentinal at the end 52 | lst.append("$3nt1n3L\n") 53 | 54 | #variables 55 | diff = [] 56 | line_num = -1 57 | pre_diff_code = "" 58 | pre_line = "" 59 | 60 | for line in lst: 61 | line_num += 1 62 | diff_code = line[0] 63 | 64 | #the content of the original line 65 | pre_line_content = pre_line[2:] 66 | line_content = line[2:] 67 | 68 | #detect a change 69 | if diff_code == '?': 70 | line_num -= 1 71 | continue 72 | elif diff_code == '+': line_num -= 1 73 | 74 | if pre_diff_code == '-' and diff_code == '+': 75 | if line_content == "" or line_content.isspace(): 76 | r = LineToDraw(line_num - 1, 0) 77 | diff.append(r) 78 | else: 79 | s = difflib.SequenceMatcher(None, pre_line_content, line_content) 80 | for tag, i1, i2, j1, j2 in s.get_opcodes(): 81 | if tag == 'insert': 82 | r = WordToDraw(line_num, j1, j2) 83 | diff.append(r) 84 | elif tag == 'delete' or tag == 'replace': 85 | r = WordToDraw(line_num, i1, i2) 86 | diff.append(r) 87 | elif pre_diff_code == '-': 88 | r = LineToDraw(line_num - 1, 0) 89 | diff.append(r) 90 | 91 | #tracking 92 | pre_line = line 93 | pre_diff_code = diff_code 94 | 95 | return diff 96 | 97 | def calculate_diff(self, text1, text2): 98 | d = difflib.Differ() 99 | result1 = list(d.compare(text1, text2)) 100 | diff_1 = self.parse_diff_list(result1) 101 | 102 | result2 = list(d.compare(text2, text1)) 103 | diff_2 = self.parse_diff_list(result2) 104 | 105 | return diff_1, diff_2 --------------------------------------------------------------------------------