295 lines
10 KiB
Python
295 lines
10 KiB
Python
# coding: utf8
|
|
import os
|
|
import re
|
|
|
|
import sublime
|
|
import sublime_plugin
|
|
import difflib
|
|
import tempfile
|
|
|
|
from fnmatch import fnmatch
|
|
import codecs
|
|
|
|
SETTINGS = sublime.load_settings('FileDiffs.sublime-settings')
|
|
|
|
CLIPBOARD = u'Diff file with Clipboard'
|
|
SELECTIONS = u'Diff Selections'
|
|
SAVED = u'Diff file with Saved'
|
|
FILE = u'Diff file with File in Project…'
|
|
TAB = u'Diff file with Open Tab…'
|
|
|
|
FILE_DIFFS = [CLIPBOARD, SAVED, FILE, TAB]
|
|
|
|
|
|
class FileDiffMenuCommand(sublime_plugin.TextCommand):
|
|
def run(self, edit):
|
|
menu_items = FILE_DIFFS[:]
|
|
saved = SAVED
|
|
non_empty_regions = [region for region in self.view.sel() if not region.empty()]
|
|
if len(non_empty_regions) == 2:
|
|
menu_items.insert(1, SELECTIONS)
|
|
elif len(non_empty_regions):
|
|
menu_items = [f.replace(u'Diff file', u'Diff selection') for f in menu_items]
|
|
saved = saved.replace(u'Diff file', u'Diff selection')
|
|
|
|
if not (self.view.file_name() and self.view.is_dirty()):
|
|
menu_items.remove(saved)
|
|
|
|
def on_done(index):
|
|
restored_menu_items = [f.replace(u'Diff selection', u'Diff file') for f in menu_items]
|
|
if index == -1:
|
|
return
|
|
elif restored_menu_items[index] == CLIPBOARD:
|
|
self.view.run_command('file_diff_clipboard')
|
|
elif restored_menu_items[index] == SELECTIONS:
|
|
self.view.run_command('file_diff_selections')
|
|
elif restored_menu_items[index] == SAVED:
|
|
self.view.run_command('file_diff_saved')
|
|
elif restored_menu_items[index] == FILE:
|
|
self.view.run_command('file_diff_file')
|
|
elif restored_menu_items[index] == TAB:
|
|
self.view.run_command('file_diff_tab')
|
|
self.view.window().show_quick_panel(menu_items, on_done)
|
|
|
|
|
|
class FileDiffCommand(sublime_plugin.TextCommand):
|
|
def diff_content(self):
|
|
content = ''
|
|
|
|
regions = [region for region in self.view.sel()]
|
|
for region in regions:
|
|
if region.empty():
|
|
continue
|
|
content += self.view.substr(region)
|
|
|
|
if not content:
|
|
content = self.view.substr(sublime.Region(0, self.view.size()))
|
|
return content
|
|
|
|
def run_diff(self, a, b, from_file=None, to_file=None):
|
|
from_content = a
|
|
to_content = b
|
|
|
|
if os.path.exists(a):
|
|
if from_file is None:
|
|
from_file = a
|
|
with codecs.open(from_file, mode='U', encoding='utf-8') as f:
|
|
from_content = f.readlines()
|
|
else:
|
|
from_content = a.splitlines(True)
|
|
if from_file is None:
|
|
from_file = 'from_file'
|
|
|
|
if os.path.exists(b):
|
|
if to_file is None:
|
|
to_file = b
|
|
with codecs.open(to_file, mode='U', encoding='utf-8') as f:
|
|
to_content = f.readlines()
|
|
else:
|
|
to_content = b.splitlines(True)
|
|
if to_file is None:
|
|
to_file = 'to_file'
|
|
|
|
diffs = list(difflib.unified_diff(from_content, to_content, from_file, to_file))
|
|
|
|
open_in_sublime = SETTINGS.get('open_in_sublime', True)
|
|
external_command = SETTINGS.get('cmd')
|
|
if not diffs:
|
|
sublime.status_message('No Difference')
|
|
else:
|
|
if external_command:
|
|
self.diff_with_external(a, b, from_file, to_file)
|
|
|
|
if open_in_sublime:
|
|
# fix diffs
|
|
diffs = map(lambda line: (line and line[-1] == "\n") and line or line + "\n", diffs)
|
|
self.diff_in_sublime(diffs)
|
|
|
|
def diff_with_external(self, a, b, from_file=None, to_file=None):
|
|
try:
|
|
if not os.path.exists(from_file):
|
|
tmp_file = tempfile.NamedTemporaryFile(delete=False)
|
|
from_file = tmp_file.name
|
|
tmp_file.close()
|
|
|
|
with codecs.open(from_file, encoding='utf-8', mode='w+') as tmp_file:
|
|
tmp_file.write(a)
|
|
|
|
if not os.path.exists(to_file):
|
|
tmp_file = tempfile.NamedTemporaryFile(delete=False)
|
|
to_file = tmp_file.name
|
|
tmp_file.close()
|
|
|
|
with codecs.open(to_file, encoding='utf-8', mode='w+') as tmp_file:
|
|
tmp_file.write(b)
|
|
|
|
if os.path.exists(from_file):
|
|
command = SETTINGS.get('cmd')
|
|
if command is not None:
|
|
command = [c.replace(u'$file1', from_file) for c in command]
|
|
command = [c.replace(u'$file2', to_file) for c in command]
|
|
self.view.window().run_command("exec", {"cmd": command})
|
|
except Exception as e:
|
|
# some basic logging here, since we are cluttering the /tmp folder
|
|
print repr(e)
|
|
sublime.status_message(str(e))
|
|
|
|
def diff_in_sublime(self, diffs):
|
|
scratch = self.view.window().new_file()
|
|
scratch.set_scratch(True)
|
|
scratch.set_syntax_file('Packages/Diff/Diff.tmLanguage')
|
|
scratch_edit = scratch.begin_edit('file_diffs')
|
|
scratch.insert(scratch_edit, 0, ''.join(diffs))
|
|
scratch.end_edit(scratch_edit)
|
|
|
|
|
|
class FileDiffClipboardCommand(FileDiffCommand):
|
|
def run(self, edit, **kwargs):
|
|
current = sublime.get_clipboard()
|
|
self.run_diff(self.diff_content(), current,
|
|
from_file=self.view.file_name(),
|
|
to_file='(clipboard)')
|
|
|
|
|
|
class FileDiffSelectionsCommand(FileDiffCommand):
|
|
def run(self, edit, **kwargs):
|
|
regions = self.view.sel()
|
|
current = self.view.substr(regions[0])
|
|
diff = self.view.substr(regions[1])
|
|
|
|
# trim off indent
|
|
indent = None
|
|
for line in current.splitlines():
|
|
new_indent = re.match('[ \t]*', line).group(0)
|
|
if new_indent == '':
|
|
continue
|
|
|
|
if indent is None:
|
|
indent = new_indent
|
|
elif len(new_indent) < len(indent):
|
|
indent = new_indent
|
|
|
|
if not indent:
|
|
break
|
|
|
|
if indent:
|
|
current = u"\n".join(line[len(indent):] for line in current.splitlines())
|
|
|
|
# trim off indent
|
|
indent = None
|
|
for line in diff.splitlines():
|
|
new_indent = re.match('[ \t]*', line).group(0)
|
|
if new_indent == '':
|
|
continue
|
|
|
|
if indent is None:
|
|
indent = new_indent
|
|
elif len(new_indent) < len(indent):
|
|
indent = new_indent
|
|
|
|
if indent:
|
|
diff = u"\n".join(line[len(indent):] for line in diff.splitlines())
|
|
|
|
self.run_diff(current, diff,
|
|
from_file='first selection',
|
|
to_file='second selection')
|
|
|
|
|
|
class FileDiffSavedCommand(FileDiffCommand):
|
|
def run(self, edit, **kwargs):
|
|
content = ''
|
|
regions = [region for region in self.view.sel()]
|
|
for region in regions:
|
|
if region.empty():
|
|
continue
|
|
content += self.view.substr(region)
|
|
if not content:
|
|
content = self.view.substr(sublime.Region(0, self.view.size()))
|
|
|
|
self.run_diff(self.view.file_name(), content,
|
|
from_file=self.view.file_name(),
|
|
to_file=self.view.file_name() + u' (Unsaved)')
|
|
|
|
|
|
class FileDiffFileCommand(FileDiffCommand):
|
|
def run(self, edit, **kwargs):
|
|
common = None
|
|
folders = self.view.window().folders()
|
|
files = self.find_files(folders)
|
|
for folder in folders:
|
|
if common == None:
|
|
common = folder
|
|
else:
|
|
common_len = len(common)
|
|
while folder[0:common_len] != common[0:common_len]:
|
|
common_len -= 1
|
|
common = common[0:common_len]
|
|
|
|
my_file = self.view.file_name()
|
|
# filter out my_file
|
|
files = [file for file in files if file != my_file]
|
|
# shorten names using common length
|
|
file_picker = [file[len(common):] for file in files]
|
|
|
|
def on_done(index):
|
|
if index > -1:
|
|
self.run_diff(self.diff_content(), files[index],
|
|
from_file=self.view.file_name())
|
|
self.view.window().show_quick_panel(file_picker, on_done)
|
|
|
|
def find_files(self, folders):
|
|
# Cannot access these settings!! WHY!?
|
|
# folder_exclude_patterns = self.view.settings().get('folder_exclude_patterns')
|
|
# file_exclude_patterns = self.view.settings().get('file_exclude_patterns')
|
|
folder_exclude_patterns = [".svn", ".git", ".hg", "CVS"]
|
|
file_exclude_patterns = ["*.pyc", "*.pyo", "*.exe", "*.dll", "*.obj", "*.o", "*.a", "*.lib", "*.so", "*.dylib", "*.ncb", "*.sdf", "*.suo", "*.pdb", "*.idb", ".DS_Store", "*.class", "*.psd", "*.db"]
|
|
|
|
ret = []
|
|
for folder in folders:
|
|
if not os.path.isdir(folder):
|
|
continue
|
|
|
|
for file in os.listdir(folder):
|
|
fullpath = os.path.join(folder, file)
|
|
if os.path.isdir(fullpath):
|
|
# excluded folder?
|
|
if not len([True for pattern in folder_exclude_patterns if fnmatch(file, pattern)]):
|
|
ret += self.find_files([fullpath])
|
|
else:
|
|
# excluded file?
|
|
if not len([True for pattern in file_exclude_patterns if fnmatch(file, pattern)]):
|
|
ret.append(fullpath)
|
|
return ret
|
|
|
|
|
|
class FileDiffTabCommand(FileDiffCommand):
|
|
def run(self, edit, **kwargs):
|
|
my_id = self.view.id()
|
|
files = []
|
|
contents = []
|
|
untitled_count = 1
|
|
for v in self.view.window().views():
|
|
if v.id() != my_id:
|
|
this_content = v.substr(sublime.Region(0, v.size()))
|
|
if v.file_name():
|
|
files.append(v.file_name())
|
|
elif v.name():
|
|
files.append(v.name())
|
|
else:
|
|
files.append('untitled %d' % untitled_count)
|
|
untitled_count += 1
|
|
|
|
contents.append(this_content)
|
|
|
|
def on_done(index):
|
|
if index > -1:
|
|
self.run_diff(self.diff_content(), contents[index],
|
|
from_file=self.view.file_name(),
|
|
to_file=files[index])
|
|
|
|
if len(files) == 1:
|
|
on_done(0)
|
|
else:
|
|
menu_items = [os.path.basename(f) for f in files]
|
|
self.view.window().show_quick_panel(menu_items, on_done)
|