Clear error message before executing statement
[reinteract/rox.git] / lib / reinteract / shell_buffer.py
blob8017494e307df18fd23d0abb2a2703e57b5f1e3d
1 #!/usr/bin/python
2 import gobject
3 import gtk
4 import traceback
5 import os
6 import re
7 from notebook import Notebook
8 from statement import Statement, ExecutionError
9 from worksheet import Worksheet
10 from custom_result import CustomResult
12 _verbose = False
14 class StatementChunk:
15 def __init__(self, start=-1, end=-1):
16 self.start = start
17 self.end = end
18 self.set_text(None)
20 self.results = None
22 self.error_message = None
23 self.error_line = None
24 self.error_offset = None
26 def __repr__(self):
27 return "StatementChunk(%d,%d,%s,%s,'%s')" % (self.start, self.end, self.needs_compile, self.needs_execute, self.text)
29 def set_text(self, text):
30 try:
31 if text == self.text:
32 return
33 except AttributeError:
34 pass
36 self.text = text
37 self.needs_compile = text != None
38 self.needs_execute = False
40 self.statement = None
42 def mark_for_execute(self):
43 if self.statement == None:
44 return
46 self.needs_execute = True
48 def compile(self, worksheet):
49 if self.statement != None:
50 return
52 self.needs_compile = False
54 self.results = None
56 self.error_message = None
57 self.error_line = None
58 self.error_offset = None
60 try:
61 self.statement = Statement(self.text, worksheet)
62 self.needs_execute = True
63 except SyntaxError, e:
64 self.error_message = e.msg
65 self.error_line = e.lineno
66 self.error_offset = e.offset
68 def execute(self, parent):
69 assert(self.statement != None)
71 self.needs_compile = False
72 self.needs_execute = False
74 self.error_message = None
75 self.error_line = None
76 self.error_offset = None
78 try:
79 self.statement.set_parent(parent)
80 self.statement.execute()
81 self.results = self.statement.results
82 except ExecutionError, e:
83 self.error_message = "\n".join(traceback.format_tb(e.traceback)[2:]) + "\n" + str(e.cause)
84 self.error_line = e.traceback.tb_frame.f_lineno
85 self.error_offset = None
87 class BlankChunk:
88 def __init__(self, start=-1, end=-1):
89 self.start = start
90 self.end = end
92 def __repr__(self):
93 return "BlankChunk(%d,%d)" % (self.start, self.end)
95 class CommentChunk:
96 def __init__(self, start=-1, end=-1):
97 self.start = start
98 self.end = end
100 def __repr__(self):
101 return "CommentChunk(%d,%d)" % (self.start, self.end)
103 class ResultChunk:
104 def __init__(self, start=-1, end=-1):
105 self.start = start
106 self.end = end
108 def __repr__(self):
109 return "ResultChunk(%d,%d)" % (self.start, self.end)
111 BLANK = re.compile(r'^\s*$')
112 COMMENT = re.compile(r'^\s*#')
113 CONTINUATION = re.compile(r'^\s+')
115 class ResultChunkFixupState:
116 pass
118 class ShellBuffer(gtk.TextBuffer, Worksheet):
119 __gsignals__ = {
120 'begin-user-action': 'override',
121 'end-user-action': 'override',
122 'insert-text': 'override',
123 'delete-range': 'override',
124 'chunk-status-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
125 'add-custom-result': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
127 # It would be more GObject to make these properties, but we'll wait on that until
128 # decent property support lands:
130 # http://blogs.gnome.org/johan/2007/04/30/simplified-gobject-properties/
132 'filename-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
133 # Clumsy naming is because GtkTextBuffer already has a modified flag, but that would
134 # include changes to the results
135 'code-modified-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
138 def __init__(self, notebook):
139 gtk.TextBuffer.__init__(self)
140 Worksheet.__init__(self, notebook)
142 self.__red_tag = self.create_tag(foreground="red")
143 self.__result_tag = self.create_tag(family="monospace", style="italic", wrap_mode=gtk.WRAP_WORD, editable=False)
144 # Order here is significant ... we want the recompute tag to have higher priority, so
145 # define it second
146 self.__error_tag = self.create_tag(foreground="#aa0000")
147 self.__recompute_tag = self.create_tag(foreground="#888888")
148 self.__comment_tag = self.create_tag(foreground="#00aa00")
149 self.__lines = [""]
150 self.__chunks = [BlankChunk(0,0)]
151 self.__modifying_results = False
152 self.__user_action_count = 0
154 self.filename = None
155 self.code_modified = False
157 def __assign_lines(self, chunk_start, lines, statement_end):
158 changed_chunks = []
160 if statement_end >= chunk_start:
161 def notnull(l): return l != None
162 text = "\n".join(filter(notnull, lines[0:statement_end + 1 - chunk_start]))
164 old_statement = None
165 for i in xrange(chunk_start, statement_end + 1):
166 if isinstance(self.__chunks[i], StatementChunk):
167 old_statement = self.__chunks[i]
168 break
170 if old_statement != None:
171 # An old statement can only be turned into *one* new statement; this
172 # prevents us getting fooled if we split a statement
173 for i in xrange(old_statement.start, old_statement.end + 1):
174 self.__chunks[i] = None
176 changed = not old_statement.needs_compile and text != old_statement.text
177 chunk = old_statement
178 else:
179 changed = True
180 chunk = StatementChunk()
182 if changed:
183 changed_chunks.append(chunk)
185 chunk.start = chunk_start
186 chunk.end = statement_end
187 chunk.set_text(text)
189 for i in xrange(chunk_start, statement_end + 1):
190 self.__chunks[i] = chunk
191 self.__lines[i] = lines[i - chunk_start]
193 for i in xrange(statement_end + 1, chunk_start + len(lines)):
194 line = lines[i - chunk_start]
196 if i > 0:
197 chunk = self.__chunks[i - 1]
198 else:
199 chunk = None
201 if line == None:
202 # a ResultChunk Must be in the before-start portion, nothing needs doing
203 pass
204 elif BLANK.match(line):
205 if not isinstance(chunk, BlankChunk):
206 chunk = BlankChunk()
207 chunk.start = i
208 chunk.end = i
209 self.__chunks[i] = chunk
210 self.__lines[i] = lines[i - chunk_start]
211 elif COMMENT.match(line):
212 if not isinstance(chunk, CommentChunk):
213 chunk = CommentChunk()
214 chunk.start = i
215 chunk.end = i
216 self.__chunks[i] = chunk
217 self.__lines[i] = lines[i - chunk_start]
218 # This is O(n^2) inefficient
219 self.__apply_tag_to_chunk(self.__comment_tag, chunk)
221 return changed_chunks
223 def __rescan(self, start_line, end_line):
224 rescan_start = start_line
225 while rescan_start > 0:
226 if rescan_start < start_line:
227 new_text = old_text = self.__lines[rescan_start]
228 else:
229 old_text = self.__lines[rescan_start]
230 i = self.get_iter_at_line(rescan_start)
231 i_end = i.copy()
232 if not i_end.ends_line():
233 i_end.forward_to_line_end()
234 new_text = self.get_slice(i, i_end)
236 if old_text == None or BLANK.match(old_text) or COMMENT.match(old_text) or CONTINUATION.match(old_text) or \
237 new_text == None or BLANK.match(new_text) or COMMENT.match(new_text) or CONTINUATION.match(new_text):
238 rescan_start -= 1
239 else:
240 break
242 # If previous contents of the modified range ended within a statement, then we need to rescan all of it;
243 # since we may have already deleted all of the statement lines within the modified range, we detect
244 # this case by seeing if the line *after* our range is a continuation line.
245 rescan_end = end_line
246 while rescan_end + 1 < len(self.__chunks):
247 if isinstance(self.__chunks[rescan_end + 1], StatementChunk) and self.__chunks[rescan_end + 1].start != rescan_end + 1:
248 rescan_end += 1
249 else:
250 break
252 chunk_start = rescan_start
253 statement_end = rescan_start - 1
254 chunk_lines = []
256 line = rescan_start
257 i = self.get_iter_at_line(rescan_start)
259 changed_chunks = []
261 for line in xrange(rescan_start, rescan_end + 1):
262 if line < start_line:
263 line_text = self.__lines[line]
264 else:
265 i_end = i.copy()
266 if not i_end.ends_line():
267 i_end.forward_to_line_end()
268 line_text = self.get_slice(i, i_end)
270 if line_text == None:
271 chunk_lines.append(line_text)
272 elif BLANK.match(line_text):
273 chunk_lines.append(line_text)
274 elif COMMENT.match(line_text):
275 chunk_lines.append(line_text)
276 elif CONTINUATION.match(line_text):
277 chunk_lines.append(line_text)
278 statement_end = line
279 else:
280 changed_chunks.extend(self.__assign_lines(chunk_start, chunk_lines, statement_end))
281 chunk_start = line
282 statement_end = line
283 chunk_lines = [line_text]
285 i.forward_line()
287 changed_chunks.extend(self.__assign_lines(chunk_start, chunk_lines, statement_end))
288 if len(changed_chunks) > 0:
289 # The the chunks in changed_chunks are already marked as needing recompilation; we
290 # need to emit signals and also mark those chunks and all subsequent chunks as
291 # needing reexecution
292 first_changed_line = changed_chunks[0].start
293 for chunk in changed_chunks:
294 if chunk.start < first_changed_line:
295 first_changed_line = chunk.start
297 for chunk in self.iterate_chunks(first_changed_line):
298 if isinstance(chunk, StatementChunk):
299 chunk.mark_for_execute()
301 result = self.__find_result(chunk)
302 if result:
303 self.__apply_tag_to_chunk(self.__recompute_tag, result)
305 self.emit("chunk-status-changed", chunk)
306 if result:
307 self.emit("chunk-status-changed", result)
310 def iterate_chunks(self, start_line=0, end_line=None):
311 if end_line == None or end_line >= len(self.__chunks):
312 end_line = len(self.__chunks) - 1
313 if start_line >= len(self.__chunks) or end_line < start_line:
314 return
316 chunk = self.__chunks[start_line]
317 while chunk == None and start_line < end_line:
318 start_line += 1
319 chunk = self.__chunks[start_line]
321 if chunk == None:
322 return
324 last_chunk = self.__chunks[end_line]
325 while last_chunk == None:
326 end_line -= 1
327 last_chunk = self.__chunks[end_line]
329 while True:
330 yield chunk
331 if chunk == last_chunk:
332 break
333 chunk = self.__chunks[chunk.end + 1]
334 while chunk == None:
335 line += 1
336 chunk = self.__chunks[line]
338 def do_begin_user_action(self):
339 self.__user_action_count += 1
341 def do_end_user_action(self):
342 self.__user_action_count -= 1
344 def do_insert_text(self, location, text, text_len):
345 start_line = location.get_line()
346 if self.__user_action_count > 0:
347 if isinstance(self.__chunks[start_line], ResultChunk):
348 return
350 if _verbose:
351 if not self.__modifying_results:
352 print "Inserting '%s' at %s" % (text, (location.get_line(), location.get_line_offset()))
354 gtk.TextBuffer.do_insert_text(self, location, text, text_len)
355 end_line = location.get_line()
357 if self.__modifying_results:
358 return
360 if self.__user_action_count > 0:
361 self.__set_modified(True)
363 result_fixup_state = self.__get_result_fixup_state(start_line, start_line)
365 self.__chunks[start_line + 1:start_line + 1] = [None for i in xrange(start_line, end_line)]
366 self.__lines[start_line + 1:start_line + 1] = [None for i in xrange(start_line, end_line)]
368 for chunk in self.iterate_chunks(start_line):
369 if chunk.start > start_line:
370 chunk.start += (end_line - start_line)
371 if chunk.end > start_line:
372 chunk.end += (end_line - start_line)
374 self.__rescan(start_line, end_line)
376 self.__fixup_results(result_fixup_state, [location])
378 if _verbose:
379 print "After insert, chunks are", self.__chunks
381 def __delete_chunk(self, chunk, revalidate_iter1=None, revalidate_iter2=None):
382 # revalidate_iter1 and revalidate_iter2 get moved to point to the location
383 # of the deleted chunk and revalidated. This is useful only as part of the
384 # workaround-hack in __fixup_results
385 self.__modifying_results = True
387 if revalidate_iter1 != None:
388 i_start = revalidate_iter1
389 i_start.set_line(chunk.start)
390 else:
391 i_start = self.get_iter_at_line(chunk.start)
392 if revalidate_iter2 != None:
393 i_end = revalidate_iter2
394 i_end.set_line(chunk.end)
395 else:
396 i_end = self.get_iter_at_line(chunk.end)
397 i_end.forward_line()
398 if i_end.get_line() == chunk.end: # Last line of buffer
399 i_end.forward_to_line_end()
400 self.delete(i_start, i_end)
402 self.__chunks[chunk.start:chunk.end + 1] = []
403 self.__lines[chunk.start:chunk.end + 1] = []
405 n_deleted = chunk.end + 1 - chunk.start
407 # Overlapping chunks can occur temporarily when inserting
408 # or deleting text merges two adjacent statements with a ResultChunk in between, so iterate
409 # all chunks, not just the ones after the deleted chunk
410 for c in self.iterate_chunks():
411 if c.end >= chunk.end:
412 c.end -= n_deleted
413 elif c.end >= chunk.start:
414 c.end = chunk.start - 1
416 if c.start >= chunk.end:
417 c.start -= n_deleted
419 self.__modifying_results = False
421 def __find_result(self, statement):
422 for chunk in self.iterate_chunks(statement.end + 1):
423 if isinstance(chunk, ResultChunk):
424 return chunk
425 elif isinstance(chunk, StatementChunk):
426 return None
428 def __get_result_fixup_state(self, first_modified_line, last_modified_line):
429 state = ResultChunkFixupState()
431 state.statement_before = None
432 state.result_before = None
433 for i in xrange(first_modified_line - 1, -1, -1):
434 if isinstance(self.__chunks[i], ResultChunk):
435 state.result_before = self.__chunks[i]
436 elif isinstance(self.__chunks[i], StatementChunk):
437 if state.result_before != None:
438 state.statement_before = self.__chunks[i]
439 break
441 state.statement_after = None
442 state.result_after = None
444 for i in xrange(last_modified_line + 1, len(self.__chunks)):
445 if isinstance(self.__chunks[i], ResultChunk):
446 state.result_after = self.__chunks[i]
447 for j in xrange(i - 1, -1, -1):
448 if isinstance(self.__chunks[j], StatementChunk):
449 state.statement_after = self.__chunks[j]
450 assert state.statement_after.results != None
451 break
452 elif isinstance(self.__chunks[i], StatementChunk) and self.__chunks[i].start == i:
453 break
455 return state
457 def __fixup_results(self, state, revalidate_iters):
458 move_before = False
459 delete_after = False
460 move_after = False
462 if state.result_before != None:
463 # If lines were added into the StatementChunk that produced the ResultChunk above the edited segment,
464 # then the ResultChunk needs to be moved after the newly inserted lines
465 if state.statement_before.end > state.result_before.start:
466 move_before = True
468 if state.result_after != None:
469 # If the StatementChunk that produced the ResultChunk after the edited segment was deleted, then the
470 # ResultChunk needs to be deleted as well
471 if self.__chunks[state.statement_after.start] != state.statement_after:
472 delete_after = True
473 else:
474 # If another StatementChunk was inserted between the StatementChunk and the ResultChunk, then we
475 # need to move the ResultChunk above that statement
476 for i in xrange(state.statement_after.end + 1, state.result_after.start):
477 if self.__chunks[i] != state.statement_after and isinstance(self.__chunks[i], StatementChunk):
478 move_after = True
480 if not (move_before or delete_after or move_after):
481 return
483 if _verbose:
484 print "Result fixups: move_before=%s, delete_after=%s, move_after=%s" % (move_before, delete_after, move_after)
486 revalidate = map(lambda iter: (iter, self.create_mark(None, iter, True)), revalidate_iters)
488 # This hack is a workaround for being unable to assign iters by value in PyGtk, see
489 # http://bugzilla.gnome.org/show_bug.cgi?id=481715
490 if len(revalidate_iters) > 0:
491 revalidate_iter = revalidate_iters[0]
492 else:
493 revalidate_iter = None
495 if len(revalidate_iters) > 1:
496 raise Exception("I don't know how to keep more than one iter valid")
498 if move_before:
499 self.__delete_chunk(state.result_before, revalidate_iter1=revalidate_iter)
500 self.insert_result(state.statement_before, revalidate_iter=revalidate_iter)
502 if delete_after or move_after:
503 self.__delete_chunk(state.result_after, revalidate_iter1=revalidate_iter)
504 if move_after:
505 self.insert_result(state.statement_after, revalidate_iter=revalidate_iter)
507 for iter, mark in revalidate:
508 new = self.get_iter_at_mark(mark)
509 iter.set_line(new.get_line())
510 iter.set_line_index(new.get_line_index())
511 self.delete_mark(mark)
513 def do_delete_range(self, start, end):
514 start_line = start.get_line()
515 end_line = end.get_line()
517 # Prevent the user from doing deletes that would merge a ResultChunk chunk into a statement
518 if self.__user_action_count > 0 and not self.__modifying_results:
519 if start.ends_line() and isinstance(self.__chunks[start_line], ResultChunk):
520 start.forward_line()
521 start_line += 1
522 if end.starts_line() and not start.starts_line() and isinstance(self.__chunks[end_line], ResultChunk):
523 end.backward_line()
524 end.forward_to_line_end()
525 end_line -= 1
527 if start.compare(end) == 0:
528 return
530 if start.starts_line() and end.starts_line():
531 (first_deleted_line, last_deleted_line) = (start_line, end_line - 1)
532 (new_start, new_end) = (start_line, start_line - 1) # empty
533 last_modified_line = end_line - 1
534 elif start.starts_line():
535 if start_line == end_line:
536 (first_deleted_line, last_deleted_line) = (start_line, start_line - 1) # empty
537 (new_start, new_end) = (start_line, start_line)
538 last_modified_line = start_line
539 else:
540 (first_deleted_line, last_deleted_line) = (start_line, end_line - 1)
541 (new_start, new_end) = (start_line, start_line)
542 last_modified_line = end_line
543 else:
544 (first_deleted_line, last_deleted_line) = (start_line + 1, end_line)
545 (new_start, new_end) = (start_line, start_line)
546 last_modified_line = end_line
548 if _verbose:
549 if not self.__modifying_results:
550 print "Deleting range %s" % (((start.get_line(), start.get_line_offset()), (end.get_line(), end.get_line_offset())),)
551 print "first_deleted_line=%d, last_deleted_line=%d, new_start=%d, new_end=%d, last_modified_line=%d" % (first_deleted_line, last_deleted_line, new_start, new_end, last_modified_line)
553 gtk.TextBuffer.do_delete_range(self, start, end)
555 if self.__modifying_results:
556 return
558 if self.__user_action_count > 0:
559 self.__set_modified(True)
561 result_fixup_state = self.__get_result_fixup_state(new_start, last_modified_line)
563 self.__chunks[first_deleted_line:last_deleted_line + 1] = []
564 self.__lines[first_deleted_line:last_deleted_line + 1] = []
565 n_deleted = 1 + last_deleted_line - first_deleted_line
567 for chunk in self.iterate_chunks():
568 if chunk.end >= last_deleted_line:
569 chunk.end -= n_deleted;
570 elif chunk.end >= first_deleted_line:
571 chunk.end = first_deleted_line - 1
573 if chunk.start >= last_deleted_line:
574 chunk.start -= n_deleted
576 self.__rescan(new_start, new_end)
578 # We can only revalidate one iter due to PyGTK limitations; see comment in __fixup_results
579 # It turns out it works to cheat and only revalidate the end iter
580 # self.__fixup_results(result_fixup_state, [start, end])
581 self.__fixup_results(result_fixup_state, [end])
583 if _verbose:
584 print "After delete, chunks are", self.__chunks
586 def calculate(self):
587 parent = None
588 have_error = False
589 for chunk in self.iterate_chunks():
590 if isinstance(chunk, StatementChunk):
591 changed = False
592 if chunk.needs_compile or (chunk.needs_execute and not have_error):
593 old_result = self.__find_result(chunk)
594 if old_result:
595 self.__delete_chunk(old_result)
597 if chunk.needs_compile:
598 changed = True
599 chunk.compile(self)
600 if chunk.error_message != None:
601 self.insert_result(chunk)
603 if chunk.needs_execute and not have_error:
604 changed = True
605 chunk.execute(parent)
606 if chunk.error_message != None:
607 self.insert_result(chunk)
608 elif len(chunk.results) > 0:
609 self.insert_result(chunk)
611 if chunk.error_message != None:
612 have_error = True
614 if changed:
615 self.emit("chunk-status-changed", chunk)
617 parent = chunk.statement
619 if _verbose:
620 print "After calculate, chunks are", self.__chunks
622 def get_chunk(self, line_index):
623 return self.__chunks[line_index]
625 def __apply_tag_to_chunk(self, tag, chunk):
626 start = self.get_iter_at_line(chunk.start)
627 end = self.get_iter_at_line(chunk.end)
628 end.forward_to_line_end()
629 self.apply_tag(tag, start,end)
631 def insert_result(self, chunk, revalidate_iter=None):
632 # revalidate_iter gets move to point to the end of the inserted result and revalidated.
633 # This is useful only as part of the workaround-hack in __fixup_results
634 self.__modifying_results = True
635 if revalidate_iter != None:
636 location = revalidate_iter
637 location.set_line(chunk.end)
638 else:
639 location = self.get_iter_at_line(chunk.end)
640 location.forward_to_line_end()
642 if chunk.error_message:
643 results = [ chunk.error_message ]
644 else:
645 results = chunk.results
647 for result in results:
648 if isinstance(result, basestring):
649 self.insert(location, "\n" + result)
650 elif isinstance(result, CustomResult):
651 self.insert(location, "\n")
652 anchor = self.create_child_anchor(location)
653 self.emit("add-custom-result", result, anchor)
655 self.__modifying_results = False
656 n_inserted = location.get_line() - chunk.end
658 result_chunk = ResultChunk(chunk.end + 1, chunk.end + n_inserted)
659 self.__chunks[chunk.end + 1:chunk.end + 1] = [result_chunk for i in xrange(0, n_inserted)]
660 self.__lines[chunk.end + 1:chunk.end + 1] = [None for i in xrange(0, n_inserted)]
662 self.__apply_tag_to_chunk(self.__result_tag, result_chunk)
664 if chunk.error_message:
665 self.__apply_tag_to_chunk(self.__error_tag, result_chunk)
667 for chunk in self.iterate_chunks(result_chunk.end + 1):
668 chunk.start += n_inserted
669 chunk.end += n_inserted
671 def __set_filename_and_modified(self, filename, modified):
672 filename_changed = filename != self.filename
673 modified_changed = modified != self.code_modified
675 if not (filename_changed or modified_changed):
676 return
678 self.filename = filename
679 self.code_modified = modified
681 if filename_changed:
682 self.emit('filename-changed')
684 if modified_changed:
685 self.emit('code-modified-changed')
687 def __set_modified(self, modified):
688 if modified == self.code_modified:
689 return
691 self.code_modified = modified
692 self.emit('code-modified-changed')
694 def __do_clear(self):
695 # This is actually working pretty much coincidentally, since the Delete
696 # code wasn't really written with non-interactive deletes in mind, and
697 # when there are ResultChunk present, a non-interactive delete will
698 # use ranges including them. But the logic happens to work out.
700 self.delete(self.get_start_iter(), self.get_end_iter())
702 def clear(self):
703 self.__do_clear()
704 self.__set_filename_and_modified(None, False)
706 def load(self, filename):
707 f = open(filename)
708 text = f.read()
709 f.close()
711 self.__do_clear()
712 self.__set_filename_and_modified(filename, False)
713 self.insert(self.get_start_iter(), text)
715 def save(self, filename=None):
716 if filename == None:
717 if self.filename == None:
718 raise ValueError("No current or specified filename")
720 filename = self.filename
722 # TODO: The atomic-save implementation here is Unix-specific and won't work on Windows
723 tmpname = filename + ".tmp"
725 # We use binary mode, since we don't want to munge line endings to the system default
726 # on a load-save cycle
727 f = open(tmpname, "wb")
729 success = False
730 try:
731 iter = self.get_start_iter()
732 for chunk in self.iterate_chunks():
733 next = iter.copy()
734 while next.get_line() <= chunk.end:
735 if not next.forward_line(): # at end of buffer
736 break
738 if not isinstance(chunk, ResultChunk):
739 chunk_text = self.get_slice(iter, next)
740 f.write(chunk_text)
742 iter = next
744 f.close()
745 os.rename(tmpname, filename)
746 success = True
748 self.__set_filename_and_modified(filename, False)
749 finally:
750 if not success:
751 f.close()
752 os.remove(tmpname)
755 if __name__ == '__main__':
756 S = StatementChunk
757 B = BlankChunk
758 C = CommentChunk
759 R = ResultChunk
761 def compare(l1, l2):
762 if len(l1) != len(l2):
763 return False
765 for i in xrange(0, len(l1)):
766 e1 = l1[i]
767 e2 = l2[i]
769 if type(e1) != type(e2) or e1.start != e2.start or e1.end != e2.end:
770 return False
772 return True
774 buffer = ShellBuffer(Notebook())
776 def expect(expected):
777 chunks = [ x for x in buffer.iterate_chunks() ]
778 if not compare(chunks, expected):
779 raise AssertionError("Got:\n %s\nExpected:\n %s" % (chunks, expected))
781 def insert(line, offset, text):
782 i = buffer.get_iter_at_line(line)
783 i.set_line_offset(offset)
784 buffer.insert(i, text)
786 def delete(start_line, start_offset, end_line, end_offset):
787 i = buffer.get_iter_at_line(start_line)
788 i.set_line_offset(start_offset)
789 j = buffer.get_iter_at_line(end_line)
790 j.set_line_offset(end_offset)
791 buffer.delete_interactive(i, j, True)
793 # Basic operation
794 insert(0, 0, "1\n\n#2\ndef a():\n 3")
795 expect([S(0,0), B(1,1), C(2,2), S(3,4)])
797 buffer.clear()
798 expect([B(0,0)])
800 # Turning a statement into a continuation line
801 insert(0, 0, "1 \\\n+ 2\n")
802 insert(1, 0, " ")
803 expect([S(0,1), B(2,2)])
805 # Calculation resulting in result chunks
806 insert(2, 0, "3\n")
807 buffer.calculate()
808 expect([S(0,1), R(2,2), S(3,3), R(4,4), B(5,5)])
810 # Check that splitting a statement with a delete results in the
811 # result chunk being moved to the last line of the first half
812 delete(1, 0, 1, 1)
813 expect([S(0,0), R(1,1), S(2,2), S(3,3), R(4,4), B(5,5)])
815 # Editing a continuation line, while leaving it a continuation
816 buffer.clear()
818 insert(0, 0, "1\\\n + 2\\\n + 3")
819 delete(1, 0, 1, 1)
820 expect([S(0,2)])
822 # Deleting an entire continuation line
823 buffer.clear()
825 insert(0, 0, "for i in (1,2):\n print i\n print i + 1\n")
826 expect([S(0,2), B(3,3)])
827 delete(1, 0, 2, 0)
828 expect([S(0,1), B(2,2)])
831 # Try writing to a file, and reading it back
833 import tempfile, os
835 buffer.clear()
836 expect([B(0,0)])
838 SAVE_TEST = """a = 1
840 # A comment
842 b = 2"""
844 insert(0, 0, SAVE_TEST)
845 buffer.calculate()
847 handle, fname = tempfile.mkstemp(".txt", "shell_buffer")
848 os.close(handle)
850 try:
851 buffer.save(fname)
852 f = open(fname, "r")
853 saved = f.read()
854 f.close()
856 if saved != SAVE_TEST:
857 raise AssertionError("Got '%s', expected '%s'", saved, SAVE_TEST)
859 buffer.load(fname)
860 buffer.calculate()
862 expect([S(0,0), S(1,1), R(2,2), C(3,3), B(4,4), S(5,5)])
863 finally:
864 os.remove(fname)
866 buffer.clear()
867 expect([B(0,0)])