-
Notifications
You must be signed in to change notification settings - Fork 161
/
history.py
230 lines (194 loc) · 8.2 KB
/
history.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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
from rope.base import exceptions, change, taskhandle
class History:
"""A class that holds project history"""
def __init__(self, project, maxundos=None):
self.project = project
self._undo_list = []
self._redo_list = []
self._maxundos = maxundos
self._load_history()
self.project.data_files.add_write_hook(self.write)
self.current_change = None
def _load_history(self):
if self.save:
result = self.project.data_files.read_data(
"history", compress=self.compress, import_=True
)
if result is not None:
to_change = change.DataToChange(self.project)
for data in result[0]:
self._undo_list.append(to_change(data))
for data in result[1]:
self._redo_list.append(to_change(data))
def do(self, changes, task_handle=taskhandle.NullTaskHandle()):
"""Perform the change and add it to the `self.undo_list`
Note that uninteresting changes (changes to ignored files)
will not be appended to `self.undo_list`.
"""
try:
self.current_change = changes
changes.do(change.create_job_set(task_handle, changes))
finally:
self.current_change = None
if self._is_change_interesting(changes):
self.undo_list.append(changes)
self._remove_extra_items()
del self.redo_list[:]
def _remove_extra_items(self):
if len(self.undo_list) > self.max_undos:
del self.undo_list[0 : len(self.undo_list) - self.max_undos]
def _is_change_interesting(self, changes):
for resource in changes.get_changed_resources():
if not self.project.is_ignored(resource):
return True
return False
def undo(self, change=None, drop=False, task_handle=taskhandle.NullTaskHandle()):
"""Redo done changes from the history
When `change` is `None`, the last done change will be undone.
If change is not `None` it should be an item from
`self.undo_list`; this change and all changes that depend on
it will be undone. In both cases the list of undone changes
will be returned.
If `drop` is `True`, the undone change will not be appended to
the redo list.
"""
if not self._undo_list:
raise exceptions.HistoryError("Undo list is empty")
if change is None:
change = self.undo_list[-1]
dependencies = self._find_dependencies(self.undo_list, change)
self._move_front(self.undo_list, dependencies)
self._perform_undos(len(dependencies), task_handle)
result = self.redo_list[-len(dependencies) :]
if drop:
del self.redo_list[-len(dependencies) :]
return result
def redo(self, change=None, task_handle=taskhandle.NullTaskHandle()):
"""Redo undone changes from the history
When `change` is `None`, the last undone change will be
redone. If change is not `None` it should be an item from
`self.redo_list`; this change and all changes that depend on
it will be redone. In both cases the list of redone changes
will be returned.
"""
if not self.redo_list:
raise exceptions.HistoryError("Redo list is empty")
if change is None:
change = self.redo_list[-1]
dependencies = self._find_dependencies(self.redo_list, change)
self._move_front(self.redo_list, dependencies)
self._perform_redos(len(dependencies), task_handle)
return self.undo_list[-len(dependencies) :]
def _move_front(self, change_list, changes):
for change in changes:
change_list.remove(change)
change_list.append(change)
def _find_dependencies(self, change_list, change):
index = change_list.index(change)
return _FindChangeDependencies(change_list[index:])()
def _perform_undos(self, count, task_handle):
for _i in range(count):
self.current_change = self.undo_list[-1]
try:
job_set = change.create_job_set(task_handle, self.current_change)
self.current_change.undo(job_set)
finally:
self.current_change = None
self.redo_list.append(self.undo_list.pop())
def _perform_redos(self, count, task_handle):
for _i in range(count):
self.current_change = self.redo_list[-1]
try:
job_set = change.create_job_set(task_handle, self.current_change)
self.current_change.do(job_set)
finally:
self.current_change = None
self.undo_list.append(self.redo_list.pop())
def contents_before_current_change(self, file):
if self.current_change is None:
return None
result = self._search_for_change_contents([self.current_change], file)
if result is not None:
return result
if file.exists() and not file.is_folder():
return file.read()
else:
return None
def _search_for_change_contents(self, change_list, file):
for change_ in reversed(change_list):
if isinstance(change_, change.ChangeSet):
result = self._search_for_change_contents(change_.changes, file)
if result is not None:
return result
if isinstance(change_, change.ChangeContents) and change_.resource == file:
return change_.old_contents
def write(self):
if self.save:
data = []
to_data = change.ChangeToData()
self._remove_extra_items()
data.append([to_data(change_) for change_ in self.undo_list])
data.append([to_data(change_) for change_ in self.redo_list])
self.project.data_files.write_data("history", data, compress=self.compress)
def get_file_undo_list(self, resource):
return [
change
for change in self.undo_list
if resource in change.get_changed_resources()
]
def __str__(self):
return "History holds %s changes in memory" % (
len(self.undo_list) + len(self.redo_list)
)
undo_list = property(lambda self: self._undo_list)
redo_list = property(lambda self: self._redo_list)
@property
def tobe_undone(self):
"""The last done change if available, `None` otherwise"""
if self.undo_list:
return self.undo_list[-1]
@property
def tobe_redone(self):
"""The last undone change if available, `None` otherwise"""
if self.redo_list:
return self.redo_list[-1]
@property
def max_undos(self):
if self._maxundos is None:
return self.project.prefs.get("max_history_items", 100)
else:
return self._maxundos
@property
def save(self):
return self.project.prefs.get("save_history", False)
@property
def compress(self):
return self.project.prefs.get("compress_history", False)
def clear(self):
"""Forget all undo and redo information"""
del self.undo_list[:]
del self.redo_list[:]
class _FindChangeDependencies:
def __init__(self, change_list):
self.change = change_list[0]
self.change_list = change_list
self.changed_resources = set(self.change.get_changed_resources())
def __call__(self):
result = [self.change]
for change in self.change_list[1:]:
if self._depends_on(change, result):
result.append(change)
self.changed_resources.update(change.get_changed_resources())
return result
def _depends_on(self, changes, result):
for resource in changes.get_changed_resources():
if resource is None:
continue
if resource in self.changed_resources:
return True
for changed in self.changed_resources:
if resource.is_folder() and resource.contains(changed):
return True
if changed.is_folder() and changed.contains(resource):
return True
return False