Simple implementation of "Copy as Doctests"
[reinteract/rox.git] / lib / reinteract / shell_buffer.py
blobe9e2e0b8d857dde463943ac79485f1e510114a66
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, WarningResult
9 from worksheet import Worksheet
10 from custom_result import CustomResult
11 import tokenize
12 from tokenized_statement import TokenizedStatement
13 from undo_stack import UndoStack, InsertOp, DeleteOp
15 # See comment in iter_copy_from.py
16 try:
17 gtk.TextIter.copy_from
18 def _copy_iter(dest, src):
19 dest.copy_from(src)
20 except AttributeError:
21 from iter_copy_from import iter_copy_from as _copy_iter
23 _verbose = False
25 class StatementChunk:
26 def __init__(self, start=-1, end=-1, nr_start=-1):
27 self.start = start
28 self.end = end
29 # this is the start index ignoring result chunks; we need this for
30 # storing items in the undo stack
31 self.nr_start = nr_start
32 self.tokenized = TokenizedStatement()
34 self.needs_compile = False
35 self.needs_execute = False
36 self.statement = None
38 self.results = None
40 self.error_message = None
41 self.error_line = None
42 self.error_offset = None
44 def __repr__(self):
45 return "StatementChunk(%d,%d,%s,%s,'%s')" % (self.start, self.end, self.needs_compile, self.needs_execute, self.tokenized.get_text())
47 def set_lines(self, lines):
48 changed_lines = self.tokenized.set_lines(lines)
49 if changed_lines == []:
50 return changed_lines
52 self.needs_compile = True
53 self.needs_execute = False
55 self.statement = None
57 return changed_lines
59 def mark_for_execute(self):
60 if self.statement == None:
61 return
63 self.needs_execute = True
65 def compile(self, worksheet):
66 if self.statement != None:
67 return
69 self.needs_compile = False
71 self.results = None
73 self.error_message = None
74 self.error_line = None
75 self.error_offset = None
77 try:
78 self.statement = Statement(self.tokenized.get_text(), worksheet)
79 self.needs_execute = True
80 except SyntaxError, e:
81 self.error_message = e.msg
82 self.error_line = e.lineno
83 self.error_offset = e.offset
85 def execute(self, parent):
86 assert(self.statement != None)
88 self.needs_compile = False
89 self.needs_execute = False
91 self.error_message = None
92 self.error_line = None
93 self.error_offset = None
95 try:
96 self.statement.set_parent(parent)
97 self.statement.execute()
98 self.results = self.statement.results
99 except ExecutionError, e:
100 self.error_message = "\n".join(traceback.format_tb(e.traceback)[2:]) + "\n".join(traceback.format_exception_only(e.type, e.value))
101 if self.error_message.endswith("\n"):
102 self.error_message = self.error_message[0:-1]
104 self.error_line = e.traceback.tb_frame.f_lineno
105 self.error_offset = None
107 class BlankChunk:
108 def __init__(self, start=-1, end=-1, nr_start=-1):
109 self.start = start
110 self.end = end
111 self.nr_start = nr_start
113 def __repr__(self):
114 return "BlankChunk(%d,%d)" % (self.start, self.end)
116 class CommentChunk:
117 def __init__(self, start=-1, end=-1, nr_start=-1):
118 self.start = start
119 self.end = end
120 self.nr_start = nr_start
122 def __repr__(self):
123 return "CommentChunk(%d,%d)" % (self.start, self.end)
125 class ResultChunk:
126 def __init__(self, start=-1, end=-1, nr_start=-1):
127 self.start = start
128 self.end = end
129 self.nr_start = nr_start
131 def __repr__(self):
132 return "ResultChunk(%d,%d)" % (self.start, self.end)
134 BLANK = re.compile(r'^\s*$')
135 COMMENT = re.compile(r'^\s*#')
136 CONTINUATION = re.compile(r'^\s+')
138 class ResultChunkFixupState:
139 pass
141 class ShellBuffer(gtk.TextBuffer, Worksheet):
142 __gsignals__ = {
143 'begin-user-action': 'override',
144 'end-user-action': 'override',
145 'insert-text': 'override',
146 'delete-range': 'override',
147 'chunk-status-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
148 'add-custom-result': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
149 'pair-location-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
151 # It would be more GObject to make these properties, but we'll wait on that until
152 # decent property support lands:
154 # http://blogs.gnome.org/johan/2007/04/30/simplified-gobject-properties/
156 'filename-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
157 # Clumsy naming is because GtkTextBuffer already has a modified flag, but that would
158 # include changes to the results
159 'code-modified-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
162 def __init__(self, notebook):
163 gtk.TextBuffer.__init__(self)
164 Worksheet.__init__(self, notebook)
166 self.__red_tag = self.create_tag(foreground="red")
167 self.__result_tag = self.create_tag(family="monospace", style="italic", wrap_mode=gtk.WRAP_WORD, editable=False)
168 # Order here is significant ... we want the recompute tag to have higher priority, so
169 # define it second
170 self.__warning_tag = self.create_tag(foreground="#aa8800")
171 self.__error_tag = self.create_tag(foreground="#aa0000")
172 self.__recompute_tag = self.create_tag(foreground="#888888")
173 self.__comment_tag = self.create_tag(foreground="#3f7f5f")
175 punctuation_tag = None
177 self.__fontify_tags = {
178 tokenize.TOKEN_KEYWORD : self.create_tag(foreground="#7f0055", weight=600),
179 tokenize.TOKEN_NAME : None,
180 tokenize.TOKEN_COMMENT : self.__comment_tag,
181 tokenize.TOKEN_BUILTIN_CONSTANT : self.create_tag(foreground="#55007f"),
182 tokenize.TOKEN_STRING : self.create_tag(foreground="#00aa00"),
183 tokenize.TOKEN_PUNCTUATION : punctuation_tag,
184 tokenize.TOKEN_CONTINUATION : punctuation_tag,
185 tokenize.TOKEN_LPAREN : punctuation_tag,
186 tokenize.TOKEN_RPAREN : punctuation_tag,
187 tokenize.TOKEN_LSQB : punctuation_tag,
188 tokenize.TOKEN_RSQB : punctuation_tag,
189 tokenize.TOKEN_LBRACE : punctuation_tag,
190 tokenize.TOKEN_RBRACE : punctuation_tag,
191 tokenize.TOKEN_BACKQUOTE : punctuation_tag,
192 tokenize.TOKEN_COLON : punctuation_tag,
193 tokenize.TOKEN_NUMBER : None,
194 tokenize.TOKEN_JUNK : self.create_tag(underline="error"),
197 self.__lines = [""]
198 self.__chunks = [BlankChunk(0,0, 0)]
199 self.__modifying_results = False
200 self.__applying_undo = False
201 self.__user_action_count = 0
203 self.__have_pair = False
204 self.__pair_mark = self.create_mark(None, self.get_start_iter(), True)
206 self.__undo_stack = UndoStack(self)
208 self.filename = None
209 self.code_modified = False
211 def __compute_nr_start(self, chunk):
212 if chunk.start == 0:
213 chunk.nr_start = 0
214 else:
215 chunk_before = self.__chunks[chunk.start - 1]
216 if isinstance(chunk_before, ResultChunk):
217 chunk.nr_start = chunk_before.nr_start
218 else:
219 chunk.nr_start = chunk_before.nr_start + (1 + chunk_before.end - chunk_before.start)
221 def __assign_lines(self, chunk_start, lines, statement_end):
222 changed_chunks = []
224 if statement_end >= chunk_start:
225 def notnull(l): return l != None
226 chunk_lines = filter(notnull, lines[0:statement_end + 1 - chunk_start])
228 old_statement = None
229 for i in xrange(chunk_start, statement_end + 1):
230 if isinstance(self.__chunks[i], StatementChunk):
231 old_statement = self.__chunks[i]
232 break
234 if old_statement != None:
235 # An old statement can only be turned into *one* new statement; this
236 # prevents us getting fooled if we split a statement
237 for i in xrange(old_statement.start, old_statement.end + 1):
238 self.__chunks[i] = None
240 chunk = old_statement
241 changed_lines = chunk.set_lines(chunk_lines)
242 changed = changed_lines != []
244 # If we moved the statement with respect to the buffer, then the we
245 # need to refontify, even if the old statement didn't change
246 if old_statement.start != chunk_start:
247 changed_lines = range(0, 1 + statement_end - chunk_start)
248 else:
249 chunk = StatementChunk()
250 changed_lines = chunk.set_lines(chunk_lines)
251 changed = True
253 if changed:
254 changed_chunks.append(chunk)
256 chunk.start = chunk_start
257 chunk.end = statement_end
258 self.__compute_nr_start(chunk)
259 self.__fontify_statement_lines(chunk, changed_lines)
261 for i in xrange(chunk_start, statement_end + 1):
262 self.__chunks[i] = chunk
263 self.__lines[i] = lines[i - chunk_start]
265 for i in xrange(statement_end + 1, chunk_start + len(lines)):
266 line = lines[i - chunk_start]
268 if i > 0:
269 chunk = self.__chunks[i - 1]
270 else:
271 chunk = None
273 if line == None:
274 # a ResultChunk Must be in the before-start portion, nothing needs doing
275 pass
276 elif BLANK.match(line):
277 if not isinstance(chunk, BlankChunk):
278 chunk = BlankChunk()
279 chunk.start = i
280 self.__compute_nr_start(chunk)
281 chunk.end = i
282 self.__chunks[i] = chunk
283 self.__lines[i] = lines[i - chunk_start]
284 elif COMMENT.match(line):
285 if not isinstance(chunk, CommentChunk):
286 chunk = CommentChunk()
287 chunk.start = i
288 self.__compute_nr_start(chunk)
289 chunk.end = i
290 self.__chunks[i] = chunk
291 self.__lines[i] = lines[i - chunk_start]
292 # This is O(n^2) inefficient
293 self.__apply_tag_to_chunk(self.__comment_tag, chunk)
295 return changed_chunks
297 def __mark_rest_for_execute(self, start_line):
298 for chunk in self.iterate_chunks(start_line):
299 if isinstance(chunk, StatementChunk):
300 chunk.mark_for_execute()
302 result = self.__find_result(chunk)
303 if result:
304 self.__apply_tag_to_chunk(self.__recompute_tag, result)
306 self.emit("chunk-status-changed", chunk)
307 if result:
308 self.emit("chunk-status-changed", result)
310 def __rescan(self, start_line, end_line, entire_statements_deleted=False):
311 rescan_start = start_line
312 while rescan_start > 0:
313 if rescan_start < start_line:
314 new_text = old_text = self.__lines[rescan_start]
315 else:
316 old_text = self.__lines[rescan_start]
317 i = self.get_iter_at_line(rescan_start)
318 i_end = i.copy()
319 if not i_end.ends_line():
320 i_end.forward_to_line_end()
321 new_text = self.get_slice(i, i_end)
323 if old_text == None or BLANK.match(old_text) or COMMENT.match(old_text) or CONTINUATION.match(old_text) or \
324 new_text == None or BLANK.match(new_text) or COMMENT.match(new_text) or CONTINUATION.match(new_text):
325 rescan_start -= 1
326 else:
327 break
329 # If previous contents of the modified range ended within a statement, then we need to rescan all of it;
330 # since we may have already deleted all of the statement lines within the modified range, we detect
331 # this case by seeing if the line *after* our range is a continuation line.
332 rescan_end = end_line
333 while rescan_end + 1 < len(self.__chunks):
334 if isinstance(self.__chunks[rescan_end + 1], StatementChunk) and self.__chunks[rescan_end + 1].start != rescan_end + 1:
335 rescan_end += 1
336 else:
337 break
339 chunk_start = rescan_start
340 statement_end = rescan_start - 1
341 chunk_lines = []
343 line = rescan_start
344 i = self.get_iter_at_line(rescan_start)
346 changed_chunks = []
348 for line in xrange(rescan_start, rescan_end + 1):
349 if line < start_line:
350 line_text = self.__lines[line]
351 else:
352 i_end = i.copy()
353 if not i_end.ends_line():
354 i_end.forward_to_line_end()
355 line_text = self.get_slice(i, i_end)
357 if line_text == None:
358 chunk_lines.append(line_text)
359 elif BLANK.match(line_text):
360 chunk_lines.append(line_text)
361 elif COMMENT.match(line_text):
362 chunk_lines.append(line_text)
363 elif CONTINUATION.match(line_text):
364 chunk_lines.append(line_text)
365 statement_end = line
366 else:
367 changed_chunks.extend(self.__assign_lines(chunk_start, chunk_lines, statement_end))
368 chunk_start = line
369 statement_end = line
370 chunk_lines = [line_text]
372 i.forward_line()
374 changed_chunks.extend(self.__assign_lines(chunk_start, chunk_lines, statement_end))
375 if len(changed_chunks) > 0:
376 # The the chunks in changed_chunks are already marked as needing recompilation; we
377 # need to emit signals and also mark those chunks and all subsequent chunks as
378 # needing reexecution
379 first_changed_line = changed_chunks[0].start
380 for chunk in changed_chunks:
381 if chunk.start < first_changed_line:
382 first_changed_line = chunk.start
384 self.__mark_rest_for_execute(first_changed_line)
385 elif entire_statements_deleted:
386 # If the user deleted entire statements we need to mark subsequent chunks
387 # as needing compilation even if all the remaining statements remained unchanged
388 self.__mark_rest_for_execute(end_line + 1)
390 def iterate_chunks(self, start_line=0, end_line=None):
391 if end_line == None or end_line >= len(self.__chunks):
392 end_line = len(self.__chunks) - 1
393 if start_line >= len(self.__chunks) or end_line < start_line:
394 return
396 chunk = self.__chunks[start_line]
397 while chunk == None and start_line < end_line:
398 start_line += 1
399 chunk = self.__chunks[start_line]
401 if chunk == None:
402 return
404 last_chunk = self.__chunks[end_line]
405 while last_chunk == None:
406 end_line -= 1
407 last_chunk = self.__chunks[end_line]
409 while True:
410 yield chunk
411 if chunk == last_chunk:
412 break
413 try:
414 line = chunk.end + 1
415 chunk = self.__chunks[line]
416 while chunk == None:
417 line += 1
418 chunk = self.__chunks[line]
419 except IndexError:
420 # This happens if the last chunk was removed; just
421 # proceeding to the end of the buffer isn't always
422 # going to be right, but it is right in the case
423 # where we are iterating the whole buffer, which
424 # is what happens for calculate()
425 return
427 def iterate_text(self, start=None, end=None):
428 result = ""
430 if start == None:
431 start = self.get_start_iter()
432 if end == None:
433 end = self.get_end_iter()
435 start_chunk = self.__chunks[start.get_line()]
436 end_chunk = self.__chunks[end.get_line()]
438 # special case .. if start/end are in the same chunk, get the text
439 # between them, even if the chunk is a ResultChunk.
440 if start_chunk == end_chunk:
441 yield self.get_slice(start, end)
442 return
444 chunk = start_chunk
445 iter = self.get_iter_at_line(chunk.start)
447 while True:
448 next_line = chunk.end + 1
449 if next_line < len(self.__chunks):
450 next_chunk = self.__chunks[chunk.end + 1]
452 next = iter.copy()
453 while next.get_line() <= chunk.end:
454 next.forward_line()
455 else:
456 next_chunk = None
457 next = iter.copy()
458 next.forward_to_end()
460 # Special case .... if the last chunk is a ResultChunk, then we don't
461 # want to include the new line from the previous line
462 if isinstance(next_chunk, ResultChunk) and next_chunk.end + 1 == len(self.__chunks):
463 next.backward_line()
464 if not next.ends_line():
465 next.forward_to_line_end()
466 next_chunk = None
468 if not isinstance(chunk, ResultChunk):
469 chunk_start, chunk_end = iter, next
470 if chunk == start_chunk:
471 chunk_start = start
472 else:
473 chunk_start = iter
475 if chunk == end_chunk:
476 chunk_end = end
477 else:
478 chunk_end = next
480 yield self.get_slice(chunk_start, chunk_end)
482 iter = next
483 line = next_line
484 if chunk == end_chunk or next_chunk == None:
485 break
486 else:
487 chunk = next_chunk
489 def get_public_text(self, start=None, end=None):
490 return "".join(self.iterate_text(start, end))
492 def do_begin_user_action(self):
493 self.__user_action_count += 1
494 self.__undo_stack.begin_user_action()
496 def do_end_user_action(self):
497 self.__user_action_count -= 1
498 self.__undo_stack.end_user_action()
500 def __compute_nr_pos_from_chunk_offset(self, chunk, line, offset):
501 if isinstance(chunk, ResultChunk):
502 prev_chunk = self.__chunks[chunk.start - 1]
503 iter = self.get_iter_at_line(prev_chunk.end)
504 if not iter.ends_line():
505 iter.forward_to_line_end()
506 return (prev_chunk.end - prev_chunk.start + prev_chunk.nr_start, iter.get_line_offset(), 1)
507 else:
508 return (line - chunk.start + chunk.nr_start, offset)
510 def __compute_nr_pos_from_iter(self, iter):
511 line = iter.get_line()
512 chunk = self.__chunks[line]
513 return self.__compute_nr_pos_from_chunk_offset(chunk, line, iter.get_line_offset())
515 def __compute_nr_pos_from_line_offset(self, line, offset):
516 return self.__compute_nr_pos_from_chunk_offset(self.__chunks[line], line, offset)
518 def _get_iter_at_nr_pos(self, nr_pos):
519 if len(nr_pos) == 2:
520 nr_line, offset = nr_pos
521 in_result = False
522 else:
523 nr_line, offset, in_result = nr_pos
525 for chunk in self.iterate_chunks():
526 if not isinstance(chunk, ResultChunk) and chunk.nr_start + (chunk.end - chunk.start) >= nr_line:
527 line = chunk.start + nr_line - chunk.nr_start
528 iter = self.get_iter_at_line(line)
529 iter.set_line_offset(offset)
531 if in_result and chunk.end + 1 < len(self.__chunks):
532 next_chunk = self.__chunks[chunk.end + 1]
533 if isinstance(next_chunk, ResultChunk):
534 iter = self.get_iter_at_line(next_chunk.end)
535 if not iter.ends_line():
536 iter.forward_to_line_end()
538 return iter
540 raise AssertionError("nr_pos pointed outside buffer")
543 def __insert_blank_line_after(self, chunk_before, location, separator):
544 start_pos = self.__compute_nr_pos_from_iter(location)
546 self.__modifying_results = True
547 gtk.TextBuffer.do_insert_text(self, location, separator, len(separator))
548 self.__modifying_results = False
550 new_chunk = BlankChunk(chunk_before.end + 1, chunk_before.end + 1, chunk_before.nr_start)
551 self.__chunks[chunk_before.end + 1:chunk_before.end + 1] = [new_chunk]
552 self.__lines[chunk_before.end + 1:chunk_before.end + 1] = [""]
554 for chunk in self.iterate_chunks(new_chunk.end + 1):
555 chunk.start += 1
556 chunk.end += 1
557 chunk.nr_start += 1
559 end_pos = self.__compute_nr_pos_from_iter(location)
560 self.__undo_stack.append_op(InsertOp(start_pos, end_pos, separator))
562 def do_insert_text(self, location, text, text_len):
563 start_line = location.get_line()
564 start_offset = location.get_line_offset()
565 is_pure_insert = False
566 if self.__user_action_count > 0:
567 current_chunk = self.__chunks[start_line]
568 if isinstance(current_chunk, ResultChunk):
569 # The only thing that's valid to do with a ResultChunk is insert
570 # a newline at the end to get another line after it
571 if not (start_line == current_chunk.end and location.ends_line()):
572 return
573 # FIXME: PS
574 if not (text.startswith("\r") or text.startswith("\n")):
575 return
577 start_line += 1
578 is_pure_insert = True
580 if _verbose:
581 if not self.__modifying_results:
582 print "Inserting '%s' at %s" % (text, (location.get_line(), location.get_line_offset()))
584 if not self.__modifying_results:
585 start_pos = self.__compute_nr_pos_from_iter(location)
587 gtk.TextBuffer.do_insert_text(self, location, text, text_len)
588 end_line = location.get_line()
589 end_offset = location.get_line_offset()
591 if self.__modifying_results:
592 return
594 if self.__user_action_count > 0:
595 self.__set_modified(True)
597 result_fixup_state = self.__get_result_fixup_state(start_line, start_line)
599 if is_pure_insert:
600 self.__chunks[start_line:start_line] = [None for i in xrange(start_line, end_line + 1)]
601 self.__lines[start_line:start_line] = [None for i in xrange(start_line, end_line + 1)]
603 for chunk in self.iterate_chunks(end_line + 1):
604 if chunk.start >= start_line:
605 chunk.start += (1 + end_line - start_line)
606 chunk.nr_start += (1 + end_line - start_line)
607 if chunk.end >= start_line:
608 chunk.end += (1 + end_line - start_line)
609 else:
610 # If we are inserting at the beginning of a line, then the insert moves the
611 # old chunk down, or leaves it in place, so insert new lines at the start position.
612 # If we insert elsewhere it either splits the chunk (and we consider
613 # that leaving the old chunk at the start) or inserts stuff after the chunk,
614 # so insert new lines after the start position.
615 if start_offset == 0:
616 self.__chunks[start_line:start_line] = [None for i in xrange(start_line, end_line)]
617 self.__lines[start_line:start_line] = [None for i in xrange(start_line, end_line)]
619 for chunk in self.iterate_chunks(start_line):
620 if chunk.start >= start_line:
621 chunk.start += (end_line - start_line)
622 chunk.nr_start += (end_line - start_line)
623 if chunk.end >= start_line:
624 chunk.end += (end_line - start_line)
625 else:
626 self.__chunks[start_line + 1:start_line + 1] = [None for i in xrange(start_line, end_line)]
627 self.__lines[start_line + 1:start_line + 1] = [None for i in xrange(start_line, end_line)]
629 for chunk in self.iterate_chunks(start_line):
630 if chunk.start > start_line:
631 chunk.start += (end_line - start_line)
632 chunk.nr_start += (end_line - start_line)
633 if chunk.end > start_line:
634 chunk.end += (end_line - start_line)
636 self.__rescan(start_line, end_line)
638 end_pos = self.__compute_nr_pos_from_line_offset(end_line, end_offset)
639 self.__undo_stack.append_op(InsertOp(start_pos, end_pos, text[0:text_len]))
641 self.__fixup_results(result_fixup_state, [location])
642 self.__calculate_pair_location()
644 if _verbose:
645 print "After insert, chunks are", self.__chunks
647 def __delete_chunk(self, chunk):
648 self.__modifying_results = True
650 i_start = self.get_iter_at_line(chunk.start)
651 i_end = self.get_iter_at_line(chunk.end)
652 i_end.forward_line()
653 if i_end.get_line() == chunk.end:
654 # Last line of buffer, need to delete the chunk and not
655 # leave a trailing newline
656 if not i_end.ends_line():
657 i_end.forward_to_line_end()
658 i_start.backward_line()
659 if not i_start.ends_line():
660 i_start.forward_to_line_end()
661 self.delete(i_start, i_end)
663 self.__chunks[chunk.start:chunk.end + 1] = []
664 self.__lines[chunk.start:chunk.end + 1] = []
666 n_deleted = chunk.end + 1 - chunk.start
667 if isinstance(chunk, ResultChunk):
668 n_nr_deleted = 0
669 else:
670 n_deleted = n_nr_deleted
672 # Overlapping chunks can occur temporarily when inserting
673 # or deleting text merges two adjacent statements with a ResultChunk in between, so iterate
674 # all chunks, not just the ones after the deleted chunk
675 for c in self.iterate_chunks():
676 if c.end >= chunk.end:
677 c.end -= n_deleted
678 elif c.end >= chunk.start:
679 c.end = chunk.start - 1
681 if c.start >= chunk.end:
682 c.start -= n_deleted
683 c.nr_start -= n_nr_deleted
685 self.__modifying_results = False
687 def __find_result(self, statement):
688 for chunk in self.iterate_chunks(statement.end + 1):
689 if isinstance(chunk, ResultChunk):
690 return chunk
691 elif isinstance(chunk, StatementChunk):
692 return None
694 def __find_statement_for_result(self, result_chunk):
695 line = result_chunk.start - 1
696 while line >= 0:
697 if isinstance(self.__chunks[line], StatementChunk):
698 return self.__chunks[line]
699 raise AssertionError("Result with no corresponding statement")
701 def __get_result_fixup_state(self, first_modified_line, last_modified_line):
702 state = ResultChunkFixupState()
704 state.statement_before = None
705 state.result_before = None
706 for i in xrange(first_modified_line - 1, -1, -1):
707 if isinstance(self.__chunks[i], ResultChunk):
708 state.result_before = self.__chunks[i]
709 elif isinstance(self.__chunks[i], StatementChunk):
710 if state.result_before != None:
711 state.statement_before = self.__chunks[i]
712 break
714 state.statement_after = None
715 state.result_after = None
717 for i in xrange(last_modified_line + 1, len(self.__chunks)):
718 if isinstance(self.__chunks[i], ResultChunk):
719 state.result_after = self.__chunks[i]
720 for j in xrange(i - 1, -1, -1):
721 if isinstance(self.__chunks[j], StatementChunk):
722 state.statement_after = self.__chunks[j]
723 assert state.statement_after.results != None or state.statement_after.error_message != None
724 break
725 elif isinstance(self.__chunks[i], StatementChunk) and self.__chunks[i].start == i:
726 break
728 return state
730 def __fixup_results(self, state, revalidate_iters):
731 move_before = False
732 delete_after = False
733 move_after = False
735 if state.result_before != None:
736 # If lines were added into the StatementChunk that produced the ResultChunk above the edited segment,
737 # then the ResultChunk needs to be moved after the newly inserted lines
738 if state.statement_before.end > state.result_before.start:
739 move_before = True
741 if state.result_after != None:
742 # If the StatementChunk that produced the ResultChunk after the edited segment was deleted, then the
743 # ResultChunk needs to be deleted as well
744 if self.__chunks[state.statement_after.start] != state.statement_after:
745 delete_after = True
746 else:
747 # If another StatementChunk was inserted between the StatementChunk and the ResultChunk, then we
748 # need to move the ResultChunk above that statement
749 for i in xrange(state.statement_after.end + 1, state.result_after.start):
750 if self.__chunks[i] != state.statement_after and isinstance(self.__chunks[i], StatementChunk):
751 move_after = True
753 if not (move_before or delete_after or move_after):
754 return
756 if _verbose:
757 print "Result fixups: move_before=%s, delete_after=%s, move_after=%s" % (move_before, delete_after, move_after)
759 revalidate = map(lambda iter: (iter, self.create_mark(None, iter, True)), revalidate_iters)
761 if move_before:
762 self.__delete_chunk(state.result_before)
763 self.insert_result(state.statement_before)
765 if delete_after or move_after:
766 self.__delete_chunk(state.result_after)
767 if move_after:
768 self.insert_result(state.statement_after)
770 for iter, mark in revalidate:
771 _copy_iter(iter, self.get_iter_at_mark(mark))
772 self.delete_mark(mark)
774 def do_delete_range(self, start, end):
776 # Note that there is a bug in GTK+ versions prior to 2.12.2, where it doesn't work
777 # if a ::delete-range handler deletes stuff outside it's requested range. (No crash,
778 # gtk_text_buffer_delete_interactive() just leaves some editable text undeleleted.)
779 # See: http://bugzilla.gnome.org/show_bug.cgi?id=491207
781 # The only workaround I can think of right now would be to stop using not-editable
782 # tags on results, and implement the editability ourselves in ::insert-text
783 # and ::delete-range, but a) that's a lot of work to rewrite that way b) it will make
784 # the text view give worse feedback. So, I'm just leaving the problem for now,
785 # (and have committed the fix to GTK+)
787 if _verbose:
788 if not self.__modifying_results:
789 print "Request to delete range %s" % (((start.get_line(), start.get_line_offset()), (end.get_line(), end.get_line_offset())),)
790 start_line = start.get_line()
791 end_line = end.get_line()
793 restore_result_statement = None
795 # Prevent the user from doing deletes that would merge a ResultChunk chunk with another chunk
796 if self.__user_action_count > 0 and not self.__modifying_results:
797 if start.ends_line() and isinstance(self.__chunks[start_line], ResultChunk):
798 # Merging another chunk onto the end of a ResultChunk; e.g., hitting delete at the
799 # start of a line with a ResultChunk before it. We don't want to actually ignore this,
800 # since otherwise if you split a line, you can't join it back up, instead we actually
801 # have to do what the user wanted to do ... join the two lines.
803 # We delete the result chunk, and if everything still looks sane at the very end,
804 # we insert it back; this is not unified with the __fixup_results() codepaths, since
805 # A) There's no insert analogue B) That's complicated enough as it is. But if we
806 # have problems, we might want to reconsider whether there is some unified way to
807 # do both. Maybe we should just delete all possibly affected ResultChunks and add
808 # them all back at the end?
810 result_chunk = self.__chunks[start_line]
811 restore_result_statement = self.__find_statement_for_result(result_chunk)
812 end_offset = end.get_line_offset()
813 self.__modifying_results = True
814 self.__delete_chunk(result_chunk)
815 self.__modifying_results = False
816 start_line -= 1 + result_chunk.end - result_chunk.start
817 end_line -= 1 + result_chunk.end - result_chunk.start
818 _copy_iter(start, self.get_iter_at_line(start_line))
819 if not start.ends_line():
820 start.forward_to_line_end()
821 _copy_iter(end, self.get_iter_at_line_offset(end_line, end_offset))
823 if end.starts_line() and not start.starts_line() and isinstance(self.__chunks[end_line], ResultChunk):
824 # Merging a ResultChunk onto the end of another chunk; just ignore this; we do have
825 # have to be careful to avoid leaving end pointing to the same place as start, since
826 # we'll then go into an infinite loop
827 new_end = end.copy()
829 new_end.backward_line()
830 if not new_end.ends_line():
831 new_end.forward_to_line_end()
833 if start.compare(new_end) == 0:
834 return
836 end.backward_line()
837 if not end.ends_line():
838 end.forward_to_line_end()
839 end_line -= 1
841 if start.starts_line() and end.starts_line():
842 (first_deleted_line, last_deleted_line) = (start_line, end_line - 1)
843 (new_start, new_end) = (start_line, start_line - 1) # empty
844 last_modified_line = end_line - 1
845 elif start.starts_line():
846 if start_line == end_line:
847 (first_deleted_line, last_deleted_line) = (start_line, start_line - 1) # empty
848 (new_start, new_end) = (start_line, start_line)
849 last_modified_line = start_line
850 else:
851 (first_deleted_line, last_deleted_line) = (start_line, end_line - 1)
852 (new_start, new_end) = (start_line, start_line)
853 last_modified_line = end_line
854 else:
855 (first_deleted_line, last_deleted_line) = (start_line + 1, end_line)
856 (new_start, new_end) = (start_line, start_line)
857 last_modified_line = end_line
859 if _verbose:
860 if not self.__modifying_results:
861 print "Deleting range %s" % (((start.get_line(), start.get_line_offset()), (end.get_line(), end.get_line_offset())),)
862 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)
864 start_pos = self.__compute_nr_pos_from_iter(start)
865 end_pos = self.__compute_nr_pos_from_iter(end)
866 deleted_text = self.get_slice(start, end)
867 gtk.TextBuffer.do_delete_range(self, start, end)
869 if self.__modifying_results:
870 return
872 if self.__user_action_count > 0:
873 self.__set_modified(True)
875 self.__undo_stack.append_op(DeleteOp(start_pos, end_pos, deleted_text))
877 result_fixup_state = self.__get_result_fixup_state(new_start, last_modified_line)
879 entire_statements_deleted = False
880 n_nr_deleted = 0
881 for chunk in self.iterate_chunks(first_deleted_line, last_deleted_line):
882 if isinstance(chunk, StatementChunk) and chunk.start >= first_deleted_line and chunk.end <= last_deleted_line:
883 entire_statements_deleted = True
885 if not isinstance(chunk, ResultChunk):
886 n_nr_deleted += 1 + min(last_deleted_line, chunk.end) - max(first_deleted_line, chunk.start)
888 n_deleted = 1 + last_deleted_line - first_deleted_line
889 self.__chunks[first_deleted_line:last_deleted_line + 1] = []
890 self.__lines[first_deleted_line:last_deleted_line + 1] = []
892 for chunk in self.iterate_chunks():
893 if chunk.end >= last_deleted_line:
894 chunk.end -= n_deleted;
895 elif chunk.end >= first_deleted_line:
896 chunk.end = first_deleted_line - 1
898 if chunk.start >= last_deleted_line:
899 chunk.start -= n_deleted
900 chunk.nr_start -= n_nr_deleted
902 self.__rescan(new_start, new_end, entire_statements_deleted=entire_statements_deleted)
904 self.__fixup_results(result_fixup_state, [start, end])
906 if restore_result_statement != None and \
907 self.__chunks[restore_result_statement.start] == restore_result_statement and \
908 self.__find_result(restore_result_statement) == None:
909 start_mark = self.create_mark(None, start, True)
910 end_mark = self.create_mark(None, end, True)
911 result_chunk = self.insert_result(restore_result_statement)
912 _copy_iter(start, self.get_iter_at_mark(start_mark))
913 self.delete_mark(start_mark)
914 _copy_iter(end, self.get_iter_at_mark(end_mark))
915 self.delete_mark(end_mark)
917 # If the cursor ended up in or after the restored result chunk,
918 # we need to move it before
919 insert = self.get_iter_at_mark(self.get_insert())
920 if insert.get_line() >= result_chunk.start:
921 insert.set_line(result_chunk.start - 1)
922 if not insert.ends_line():
923 insert.forward_to_line_end()
924 self.place_cursor(insert)
926 self.__calculate_pair_location()
928 if _verbose:
929 print "After delete, chunks are", self.__chunks
931 def calculate(self):
932 parent = None
933 have_error = False
934 for chunk in self.iterate_chunks():
935 if isinstance(chunk, StatementChunk):
936 changed = False
937 if chunk.needs_compile or (chunk.needs_execute and not have_error):
938 old_result = self.__find_result(chunk)
939 if old_result:
940 self.__delete_chunk(old_result)
942 if chunk.needs_compile:
943 changed = True
944 chunk.compile(self)
945 if chunk.error_message != None:
946 self.insert_result(chunk)
948 if chunk.needs_execute and not have_error:
949 changed = True
950 chunk.execute(parent)
951 if chunk.error_message != None:
952 self.insert_result(chunk)
953 elif len(chunk.results) > 0:
954 self.insert_result(chunk)
956 if chunk.error_message != None:
957 have_error = True
959 if changed:
960 self.emit("chunk-status-changed", chunk)
962 parent = chunk.statement
964 if _verbose:
965 print "After calculate, chunks are", self.__chunks
967 def __set_pair_location(self, location):
968 changed = False
969 old_location = None
971 if location == None:
972 if self.__have_pair:
973 old_location = self.get_iter_at_mark(self.__pair_mark)
974 self.__have_pair = False
975 changed = True
976 else:
977 if not self.__have_pair:
978 self.__have_pair = True
979 self.move_mark(self.__pair_mark, location)
980 changed = True
981 else:
982 old_location = self.get_iter_at_mark(self.__pair_mark)
983 if location.compare(old_location) != 0:
984 self.move_mark(self.__pair_mark, location)
985 changed = True
987 if changed:
988 self.emit('pair-location-changed', old_location, location)
990 def get_pair_location(self):
991 if self.__have_pair:
992 return self.get_iter_at_mark(self.__pair_mark)
993 else:
994 return None
996 def __calculate_pair_location(self):
997 location = self.get_iter_at_mark(self.get_insert())
999 # GTK+-2.10 has fractionally-more-efficient buffer.get_has_selection()
1000 selection_bound = self.get_iter_at_mark(self.get_selection_bound())
1001 if location.compare(selection_bound) != 0:
1002 self.__set_pair_location(None)
1003 return
1005 location = self.get_iter_at_mark(self.get_insert())
1007 line = location.get_line()
1008 chunk = self.__chunks[line]
1009 if not isinstance(chunk, StatementChunk):
1010 self.__set_pair_location(None)
1011 return
1013 if location.starts_line():
1014 self.__set_pair_location(None)
1015 return
1017 previous = location.copy()
1018 previous.backward_char()
1019 pair_line, pair_start = chunk.tokenized.get_pair_location(line - chunk.start, previous.get_line_index())
1021 if pair_line == None:
1022 self.__set_pair_location(None)
1023 return
1025 pair_iter = self.get_iter_at_line_index(chunk.start + pair_line, pair_start)
1026 self.__set_pair_location(pair_iter)
1028 def do_mark_set(self, location, mark):
1029 try:
1030 gtk.TextBuffer.do_mark_set(self, location, mark)
1031 except NotImplementedError:
1032 # the default handler for ::mark-set was added in GTK+-2.10
1033 pass
1035 if mark != self.get_insert() and mark != self.get_selection_bound():
1036 return
1038 self.__calculate_pair_location()
1040 def get_chunk(self, line_index):
1041 return self.__chunks[line_index]
1043 def undo(self):
1044 self.__undo_stack.undo()
1046 def redo(self):
1047 self.__undo_stack.redo()
1049 def __get_chunk_bounds(self, chunk):
1050 start = self.get_iter_at_line(chunk.start)
1051 end = self.get_iter_at_line(chunk.end)
1052 if not end.ends_line():
1053 end.forward_to_line_end()
1054 return start, end
1056 def copy_as_doctests(self, clipboard):
1057 bounds = self.get_selection_bounds()
1058 if bounds == ():
1059 start, end = self.get_iter_at_mark(self.get_insert())
1060 else:
1061 start, end = bounds
1063 result = ""
1064 for chunk in self.iterate_chunks(start.get_line(), end.get_line()):
1065 chunk_text = self.get_text(*self.__get_chunk_bounds(chunk))
1067 if isinstance(chunk, ResultChunk) or isinstance(chunk, BlankChunk):
1068 if chunk.end == len(self.__chunks) - 1:
1069 result += chunk_text
1070 else:
1071 result += chunk_text + "\n"
1072 else:
1073 first = True
1074 for line in chunk_text.split("\n"):
1075 if isinstance(chunk, StatementChunk) and not first:
1076 result += "... " + line + "\n"
1077 else:
1078 result += ">>> " + line + "\n"
1079 first = False
1081 clipboard.set_text(result)
1083 def __fontify_statement_lines(self, chunk, changed_lines):
1084 iter = self.get_iter_at_line(chunk.start)
1085 i = 0
1086 for l in changed_lines:
1087 while i < l:
1088 iter.forward_line()
1089 i += 1
1090 end = iter.copy()
1091 end.forward_line()
1092 self.remove_all_tags(iter, end)
1094 end = iter.copy()
1095 for token_type, start_index, end_index, _ in chunk.tokenized.get_tokens(l):
1096 tag = self.__fontify_tags[token_type]
1097 if tag != None:
1098 iter.set_line_index(start_index)
1099 end.set_line_index(end_index)
1100 self.apply_tag(tag, iter, end)
1102 def __apply_tag_to_chunk(self, tag, chunk):
1103 start, end = self.__get_chunk_bounds(chunk)
1104 self.apply_tag(tag, start, end)
1106 def __remove_tag_from_chunk(self, tag, chunk):
1107 start, end = self.__get_chunk_bounds(chunk)
1108 self.remove_tag(tag, start, end)
1110 def insert_result(self, chunk):
1111 self.__modifying_results = True
1112 location = self.get_iter_at_line(chunk.end)
1113 if not location.ends_line():
1114 location.forward_to_line_end()
1116 if chunk.error_message:
1117 results = [ chunk.error_message ]
1118 else:
1119 results = chunk.results
1121 # We don't want to move the insert cursor in the common case of
1122 # inserting a result right at the insert cursor
1123 if location.compare(self.get_iter_at_mark(self.get_insert())) == 0:
1124 saved_insert = self.create_mark(None, location, True)
1125 else:
1126 saved_insert = None
1128 for result in results:
1129 if isinstance(result, basestring):
1130 self.insert(location, "\n" + result)
1131 elif isinstance(result, WarningResult):
1132 start_mark = self.create_mark(None, location, True)
1133 self.insert(location, "\n" + result.message)
1134 start = self.get_iter_at_mark(start_mark)
1135 self.delete_mark(start_mark)
1136 self.apply_tag(self.__warning_tag, start, location)
1137 elif isinstance(result, CustomResult):
1138 self.insert(location, "\n")
1139 anchor = self.create_child_anchor(location)
1140 self.emit("add-custom-result", result, anchor)
1142 self.__modifying_results = False
1143 n_inserted = location.get_line() - chunk.end
1145 result_chunk = ResultChunk(chunk.end + 1, chunk.end + n_inserted)
1146 self.__compute_nr_start(result_chunk)
1147 self.__chunks[chunk.end + 1:chunk.end + 1] = [result_chunk for i in xrange(0, n_inserted)]
1148 self.__lines[chunk.end + 1:chunk.end + 1] = [None for i in xrange(0, n_inserted)]
1150 self.__apply_tag_to_chunk(self.__result_tag, result_chunk)
1152 if chunk.error_message:
1153 self.__apply_tag_to_chunk(self.__error_tag, result_chunk)
1155 for chunk in self.iterate_chunks(result_chunk.end + 1):
1156 chunk.start += n_inserted
1157 chunk.end += n_inserted
1159 if saved_insert != None:
1160 self.place_cursor(self.get_iter_at_mark(saved_insert))
1161 self.delete_mark(saved_insert)
1163 return result_chunk
1165 def __set_filename_and_modified(self, filename, modified):
1166 filename_changed = filename != self.filename
1167 modified_changed = modified != self.code_modified
1169 if not (filename_changed or modified_changed):
1170 return
1172 self.filename = filename
1173 self.code_modified = modified
1175 if filename_changed:
1176 self.emit('filename-changed')
1178 if modified_changed:
1179 self.emit('code-modified-changed')
1181 def __set_modified(self, modified):
1182 if modified == self.code_modified:
1183 return
1185 self.code_modified = modified
1186 self.emit('code-modified-changed')
1188 def __do_clear(self):
1189 # This is actually working pretty much coincidentally, since the Delete
1190 # code wasn't really written with non-interactive deletes in mind, and
1191 # when there are ResultChunk present, a non-interactive delete will
1192 # use ranges including them. But the logic happens to work out.
1194 self.delete(self.get_start_iter(), self.get_end_iter())
1196 def clear(self):
1197 self.__do_clear()
1198 self.__set_filename_and_modified(None, False)
1200 # This prevents redoing New, but we need some more work to enable that
1201 self.__undo_stack.clear()
1203 def load(self, filename):
1204 f = open(filename)
1205 text = f.read()
1206 f.close()
1208 self.__do_clear()
1209 self.__set_filename_and_modified(filename, False)
1210 self.insert(self.get_start_iter(), text)
1211 self.__undo_stack.clear()
1213 def save(self, filename=None):
1214 if filename == None:
1215 if self.filename == None:
1216 raise ValueError("No current or specified filename")
1218 filename = self.filename
1220 # TODO: The atomic-save implementation here is Unix-specific and won't work on Windows
1221 tmpname = filename + ".tmp"
1223 # We use binary mode, since we don't want to munge line endings to the system default
1224 # on a load-save cycle
1225 f = open(tmpname, "wb")
1227 success = False
1228 try:
1229 for chunk_text in self.iterate_text():
1230 f.write(chunk_text)
1232 f.close()
1233 os.rename(tmpname, filename)
1234 success = True
1236 self.__set_filename_and_modified(filename, False)
1237 finally:
1238 if not success:
1239 f.close()
1240 os.remove(tmpname)
1242 if __name__ == '__main__':
1243 S = StatementChunk
1244 B = BlankChunk
1245 C = CommentChunk
1246 R = ResultChunk
1248 def compare(l1, l2):
1249 if len(l1) != len(l2):
1250 return False
1252 for i in xrange(0, len(l1)):
1253 e1 = l1[i]
1254 e2 = l2[i]
1256 if type(e1) != type(e2) or e1.start != e2.start or e1.end != e2.end:
1257 return False
1259 return True
1261 buffer = ShellBuffer(Notebook())
1263 def validate_nr_start():
1264 n_nr = 0
1265 for chunk in buffer.iterate_chunks():
1266 if chunk.nr_start != n_nr:
1267 raise AssertionError("nr_start for chunk %s should have been %d but is %d" % (chunk, n_nr, chunk.nr_start))
1268 assert(chunk.nr_start == n_nr)
1269 if not isinstance(chunk, ResultChunk):
1270 n_nr += 1 + chunk.end - chunk.start
1272 def expect(expected):
1273 chunks = [ x for x in buffer.iterate_chunks() ]
1274 if not compare(chunks, expected):
1275 raise AssertionError("\nGot:\n %s\nExpected:\n %s" % (chunks, expected))
1276 validate_nr_start()
1278 def expect_text(expected, start_line=None, start_offset=None, end_line=None, end_offset=None):
1279 if start_offset != None:
1280 i = buffer.get_iter_at_line_offset(start_line, start_offset)
1281 else:
1282 i = None
1284 if end_offset != None:
1285 j = buffer.get_iter_at_line_offset(end_line, end_offset)
1286 else:
1287 j = None
1289 text = buffer.get_public_text(i, j)
1290 if (text != expected):
1291 raise AssertionError("\nGot:\n '%s'\nExpected:\n '%s'" % (text, expected))
1293 def insert(line, offset, text):
1294 i = buffer.get_iter_at_line_offset(line, offset)
1295 buffer.insert_interactive(i, text, True)
1297 def delete(start_line, start_offset, end_line, end_offset):
1298 i = buffer.get_iter_at_line_offset(start_line, start_offset)
1299 j = buffer.get_iter_at_line_offset(end_line, end_offset)
1300 buffer.delete_interactive(i, j, True)
1302 def clear():
1303 buffer.clear()
1305 # Basic operation
1306 insert(0, 0, "1\n\n#2\ndef a():\n 3")
1307 expect([S(0,0), B(1,1), C(2,2), S(3,4)])
1309 clear()
1310 expect([B(0,0)])
1312 # Turning a statement into a continuation line
1313 insert(0, 0, "1 \\\n+ 2\n")
1314 insert(1, 0, " ")
1315 expect([S(0,1), B(2,2)])
1317 # Calculation resulting in result chunks
1318 insert(2, 0, "3\n")
1319 buffer.calculate()
1320 expect([S(0,1), R(2,2), S(3,3), R(4,4), B(5,5)])
1322 # Check that splitting a statement with a delete results in the
1323 # result chunk being moved to the last line of the first half
1324 delete(1, 0, 1, 1)
1325 expect([S(0,0), R(1,1), S(2,2), S(3,3), R(4,4), B(5,5)])
1327 # Editing a continuation line, while leaving it a continuation
1328 clear()
1330 insert(0, 0, "1\\\n + 2\\\n + 3")
1331 delete(1, 0, 1, 1)
1332 expect([S(0,2)])
1334 # Editing a line with an existing error chunk to fix the error
1335 clear()
1337 insert(0, 0, "a\na=2")
1338 buffer.calculate()
1340 insert(0, 0, "2")
1341 delete(0, 1, 0, 2)
1342 buffer.calculate()
1343 expect([S(0,0), R(1,1), S(2,2)])
1345 # Deleting an entire continuation line
1346 clear()
1348 insert(0, 0, "for i in (1,2):\n print i\n print i + 1\n")
1349 expect([S(0,2), B(3,3)])
1350 delete(1, 0, 2, 0)
1351 expect([S(0,1), B(2,2)])
1353 # Test an attempt to join a ResultChunk onto a previous chunk; should ignore
1354 clear()
1356 insert(0, 0, "1\n");
1357 buffer.calculate()
1358 expect([S(0,0), R(1,1), B(2,2)])
1359 delete(0, 1, 1, 0)
1360 expect_text("1\n");
1362 # Test an attempt to join a chunk onto a previous ResultChunk, should move
1363 # the ResultChunk and do the modification
1364 clear()
1366 insert(0, 0, "1\n2\n");
1367 buffer.calculate()
1368 expect([S(0,0), R(1,1), S(2,2), R(3,3), B(4,4)])
1369 delete(1, 1, 2, 0)
1370 expect([S(0,0), R(1,1), B(2,2)])
1371 expect_text("12\n");
1373 # Test inserting random text inside a result chunk, should ignore
1374 clear()
1376 insert(0, 0, "1\n2");
1377 buffer.calculate()
1378 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
1379 insert(1, 0, "foo")
1380 expect_text("1\n2");
1381 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
1383 # Test inserting a newline at the end of a result chunk, should create
1384 # a new line
1385 insert(1, 1, "\n")
1386 expect_text("1\n\n2");
1387 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4)])
1389 # Same, at the end of the buffer
1390 insert(4, 1, "\n")
1391 expect_text("1\n\n2\n");
1392 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4), B(5,5)])
1394 # Try undoing these insertions
1395 buffer.undo()
1396 expect_text("1\n\n2");
1397 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4)])
1399 buffer.undo()
1400 expect_text("1\n2");
1401 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
1403 # Calculation resulting in a multi-line result change
1404 clear()
1406 insert(0, 0, "for i in range(0, 10): print i")
1407 buffer.calculate()
1408 expect([S(0, 0), R(1, 10)])
1410 # Test deleting a range containing both results and statements
1412 clear()
1414 insert(0, 0, "1\n2\n3\n4\n")
1415 buffer.calculate()
1416 expect([S(0,0), R(1,1), S(2,2), R(3,3), S(4,4), R(5,5), S(6,6), R(7,7), B(8,8)])
1418 delete(2, 0, 5, 0)
1419 expect([S(0,0), R(1,1), S(2,2), R(3,3), B(4,4)])
1421 # Inserting an entire new statement in the middle
1422 insert(2, 0, "2.5\n")
1423 expect([S(0,0), R(1,1), S(2,2), S(3,3), R(4,4), B(5,5)])
1424 buffer.calculate()
1425 expect([S(0,0), R(1,1), S(2,2), R(3, 3), S(4, 4), R(5,5), B(6,6)])
1427 # Check that inserting a blank line at the beginning of a statement leaves
1428 # the result behind
1429 insert(2, 0, "\n")
1430 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4), S(5,5), R(6,6), B(7,7)])
1432 # Test deleting a range including a result and joining two statements
1433 clear()
1434 insert(0, 0, "12\n34")
1435 buffer.calculate()
1436 delete(0, 1, 2, 1)
1437 expect_text("14")
1439 # Undo tests
1440 clear()
1442 insert(0, 0, "1")
1443 buffer.undo()
1444 expect_text("")
1445 buffer.redo()
1446 expect_text("1")
1448 # Undoing insertion of a newline
1449 clear()
1451 insert(0, 0, "1 ")
1452 insert(0, 1, "\n")
1453 buffer.calculate()
1454 buffer.undo()
1455 expect_text("1 ")
1457 # Test the "pruning" behavior of modifications after undos
1458 clear()
1460 insert(0, 0, "1")
1461 buffer.undo()
1462 expect_text("")
1463 insert(0, 0, "2")
1464 buffer.redo() # does nothing
1465 expect_text("2")
1466 insert(0, 0, "2\n")
1468 # Test coalescing consecutive inserts
1469 clear()
1471 insert(0, 0, "1")
1472 insert(0, 1, "2")
1473 buffer.undo()
1474 expect_text("")
1476 # Test grouping of multiple undos by user actions
1477 clear()
1479 insert(0, 0, "1")
1480 buffer.begin_user_action()
1481 delete(0, 0, 0, 1)
1482 insert(0, 0, "2")
1483 buffer.end_user_action()
1484 buffer.undo()
1485 expect_text("1")
1486 buffer.redo()
1487 expect_text("2")
1489 # Make sure that coalescing doesn't coalesce one user action with
1490 # only part of another
1491 clear()
1493 insert(0, 0, "1")
1494 buffer.begin_user_action()
1495 insert(0, 1, "2")
1496 delete(0, 0, 0, 1)
1497 buffer.end_user_action()
1498 buffer.undo()
1499 expect_text("1")
1500 buffer.redo()
1501 expect_text("2")
1503 # Test an undo of an insert that caused insertion of result chunks
1504 clear()
1506 insert(0, 0, "2\n")
1507 expect([S(0,0), B(1,1)])
1508 buffer.calculate()
1509 expect([S(0,0), R(1,1), B(2,2)])
1510 insert(0, 0, "1\n")
1511 buffer.calculate()
1512 buffer.undo()
1513 expect([S(0,0), R(1,1), B(2,2)])
1514 expect_text("2\n")
1516 # Tests of get_public_text()
1517 clear()
1518 insert(0, 0, "12\n34\n56")
1519 buffer.calculate()
1521 expect_text("12\n34\n56", 0, 0, 5, 2)
1522 expect_text("4\n5", 2, 1, 4, 1)
1524 # within a single result get_public_text() *does* include the text of the result
1525 expect_text("1", 1, 0, 1, 1)
1528 # Try writing to a file, and reading it back
1530 import tempfile, os
1532 clear()
1533 expect([B(0,0)])
1535 SAVE_TEST = """a = 1
1537 # A comment
1539 b = 2"""
1541 insert(0, 0, SAVE_TEST)
1542 buffer.calculate()
1544 handle, fname = tempfile.mkstemp(".txt", "shell_buffer")
1545 os.close(handle)
1547 try:
1548 buffer.save(fname)
1549 f = open(fname, "r")
1550 saved = f.read()
1551 f.close()
1553 if saved != SAVE_TEST:
1554 raise AssertionError("Got '%s', expected '%s'", saved, SAVE_TEST)
1556 buffer.load(fname)
1557 buffer.calculate()
1559 expect([S(0,0), S(1,1), R(2,2), C(3,3), B(4,4), S(5,5)])
1560 finally:
1561 os.remove(fname)
1563 clear()
1564 expect([B(0,0)])