-
Notifications
You must be signed in to change notification settings - Fork 1
/
zipcontents.py
159 lines (121 loc) · 5.84 KB
/
zipcontents.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import re
from collections import defaultdict
from tempfile import NamedTemporaryFile
from zipfile import ZipFile
import sublime
from sublime_plugin import EventListener
from .viewio import HexViewIO
class ZipFileListener(EventListener):
_ZIP_SIGNATURES = ("504b 0304", "504b 0506", "504b 00708")
_OVERLAY_PANEL_ELEMENTS = ("command_palette:input", "goto_anything:input", "quick_panel:input")
def __init__(self):
if int(sublime.version()) < 4050:
self._is_overlay_panel = self._is_overlay_panel_heuristic
self._overlay_panel_open = False
self._zip_view_awaiting_panel_close = None
def on_init(self, views):
self._overlay_panel_open = self._is_overlay_panel(sublime.active_window().active_view())
@classmethod
def _is_overlay_panel(cls, view):
return view.element() in cls._OVERLAY_PANEL_ELEMENTS
@staticmethod
def _is_overlay_panel_heuristic(view):
return view.settings().get("is_widget")
def on_activated(self, view):
if self._is_overlay_panel(view):
self._overlay_panel_open = True
def on_deactivated(self, view):
if self._is_overlay_panel(view):
self._overlay_panel_open = False
self._show_zip_contents_awaiting_panel_close()
def _show_zip_contents_awaiting_panel_close(self):
if self._zip_view_awaiting_panel_close:
zip_contents = ZipContents(self, self._zip_view_awaiting_panel_close)
self._zip_view_awaiting_panel_close = None
# A quick panel won't show if an overlay panel is in the process of closing.
sublime.set_timeout(zip_contents.show, 0)
def on_load(self, view):
if view.encoding() == "Hexadecimal" and self._view_starts_with_zip_signature(view):
if self._overlay_panel_open:
self._zip_view_awaiting_panel_close = view
else:
ZipContents(self, view).show()
@classmethod
def _view_starts_with_zip_signature(cls, view):
signature_region = sublime.Region(0, len(cls._ZIP_SIGNATURES[0]))
return view.substr(signature_region) in cls._ZIP_SIGNATURES
def on_close(self, view):
if view == self._zip_view_awaiting_panel_close:
self._zip_view_awaiting_panel_close = None
class ZipContents:
def __init__(self, listener, view):
self._settings = sublime.load_settings("ZipContents.sublime-settings")
self._view = view
self._window = view.window()
self._file = ZipFile(HexViewIO(view))
self._contents = self._zip_contents()
def _zip_contents(self):
# Remove folder-only entries.
contents = [file_path for file_path in self._file.namelist() if not file_path.endswith("/")]
# Remove entries that match file and folder exclude patterns.
file_exclude_patterns = self._settings.get("file_exclude_patterns")
folder_exclude_patterns = self._settings.get("folder_exclude_patterns")
if file_exclude_patterns or folder_exclude_patterns:
exclude_pattern = self._compile_exclude_pattern(
file_exclude_patterns, folder_exclude_patterns
)
contents = [
file_path for file_path in contents if not exclude_pattern.search(file_path)
]
return sorted(contents)
@classmethod
def _compile_exclude_pattern(cls, file_exclude_patterns, folder_exclude_patterns):
patterns = []
if file_exclude_patterns: # File patterns are followed by end of string.
patterns += [cls._convert_pattern(pattern) + "$" for pattern in file_exclude_patterns]
if folder_exclude_patterns: # Folder patterns are followed by slash.
patterns += [cls._convert_pattern(pattern) + "/" for pattern in folder_exclude_patterns]
# Match beginning of string or slash, followed by any pattern.
return re.compile("(?:^|/)(?:" + "|".join(patterns) + ")")
@staticmethod
def _convert_pattern(pattern):
# Convert "*" and "?" to "[^/]*" and "[^/]" and escape everything else.
pattern = pattern.replace("*", "__zipcontentsstar__")
pattern = pattern.replace("?", "__zipcontentsquestion__")
pattern = re.escape(pattern)
pattern = pattern.replace("__zipcontentsstar__", "[^/]*")
pattern = pattern.replace("__zipcontentsquestion__", "[^/]")
return pattern
def show(self):
self._window.show_quick_panel(self._contents, self._extract_file)
def _extract_file(self, index):
# Do nothing if no item was selected in the quick panel.
if index == -1:
return
file_path = self._contents[index]
file_name = file_path.split("/").pop()
temp_file = NamedTemporaryFile(suffix=file_name)
temp_file.write(self._file.read(file_path))
temp_file.flush()
self._view.close()
extracted_file_view = self._window.open_file(temp_file.name)
ExtractedFileListener.set_name_on_load(extracted_file_view, file_name)
ExtractedFileListener.set_scratch_on_load(extracted_file_view)
ExtractedFileListener.close_file_on_load(extracted_file_view, temp_file)
class ExtractedFileListener(EventListener):
_load_callbacks = defaultdict(list)
@classmethod
def set_name_on_load(cls, view, name):
cls._load_callbacks[view.id()].append(lambda: view.set_name(name))
@classmethod
def set_scratch_on_load(cls, view):
cls._load_callbacks[view.id()].append(lambda: view.set_scratch(True))
@classmethod
def close_file_on_load(cls, view, file):
cls._load_callbacks[view.id()].append(lambda: file.close())
def on_load(self, view):
view_id = view.id()
if view_id in self._load_callbacks:
for callback in self._load_callbacks[view_id]:
callback()
del self._load_callbacks[view_id]