├── .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
--------------------------------------------------------------------------------