7 from notebook
import Notebook
8 from statement
import Statement
, ExecutionError
, WarningResult
9 from worksheet
import Worksheet
10 from custom_result
import CustomResult
12 from tokenized_statement
import TokenizedStatement
13 from undo_stack
import UndoStack
, InsertOp
, DeleteOp
15 # See comment in iter_copy_from.py
17 gtk
.TextIter
.copy_from
18 def _copy_iter(dest
, src
):
20 except AttributeError:
21 from iter_copy_from
import iter_copy_from
as _copy_iter
26 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
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
40 self
.error_message
= None
41 self
.error_line
= None
42 self
.error_offset
= None
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
== []:
52 self
.needs_compile
= True
53 self
.needs_execute
= False
59 def mark_for_execute(self
):
60 if self
.statement
== None:
63 self
.needs_execute
= True
65 def compile(self
, worksheet
):
66 if self
.statement
!= None:
69 self
.needs_compile
= False
73 self
.error_message
= None
74 self
.error_line
= None
75 self
.error_offset
= None
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
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
108 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
111 self
.nr_start
= nr_start
114 return "BlankChunk(%d,%d)" % (self
.start
, self
.end
)
117 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
120 self
.nr_start
= nr_start
123 return "CommentChunk(%d,%d)" % (self
.start
, self
.end
)
126 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
129 self
.nr_start
= nr_start
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
:
141 class ShellBuffer(gtk
.TextBuffer
, Worksheet
):
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
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"),
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
)
209 self
.code_modified
= False
211 def __compute_nr_start(self
, chunk
):
215 chunk_before
= self
.__chunks
[chunk
.start
- 1]
216 if isinstance(chunk_before
, ResultChunk
):
217 chunk
.nr_start
= chunk_before
.nr_start
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
):
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
])
229 for i
in xrange(chunk_start
, statement_end
+ 1):
230 if isinstance(self
.__chunks
[i
], StatementChunk
):
231 old_statement
= self
.__chunks
[i
]
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
)
249 chunk
= StatementChunk()
250 changed_lines
= chunk
.set_lines(chunk_lines
)
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
]
269 chunk
= self
.__chunks
[i
- 1]
274 # a ResultChunk Must be in the before-start portion, nothing needs doing
276 elif BLANK
.match(line
):
277 if not isinstance(chunk
, BlankChunk
):
280 self
.__compute
_nr
_start
(chunk
)
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()
288 self
.__compute
_nr
_start
(chunk
)
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
)
304 self
.__apply
_tag
_to
_chunk
(self
.__recompute
_tag
, result
)
306 self
.emit("chunk-status-changed", chunk
)
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
]
316 old_text
= self
.__lines
[rescan_start
]
317 i
= self
.get_iter_at_line(rescan_start
)
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
):
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:
339 chunk_start
= rescan_start
340 statement_end
= rescan_start
- 1
344 i
= self
.get_iter_at_line(rescan_start
)
348 for line
in xrange(rescan_start
, rescan_end
+ 1):
349 if line
< start_line
:
350 line_text
= self
.__lines
[line
]
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
)
367 changed_chunks
.extend(self
.__assign
_lines
(chunk_start
, chunk_lines
, statement_end
))
370 chunk_lines
= [line_text
]
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
:
396 chunk
= self
.__chunks
[start_line
]
397 while chunk
== None and start_line
< end_line
:
399 chunk
= self
.__chunks
[start_line
]
404 last_chunk
= self
.__chunks
[end_line
]
405 while last_chunk
== None:
407 last_chunk
= self
.__chunks
[end_line
]
411 if chunk
== last_chunk
:
415 chunk
= self
.__chunks
[line
]
418 chunk
= self
.__chunks
[line
]
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()
427 def iterate_text(self
, start
=None, end
=None):
431 start
= self
.get_start_iter()
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
)
445 iter = self
.get_iter_at_line(chunk
.start
)
448 next_line
= chunk
.end
+ 1
449 if next_line
< len(self
.__chunks
):
450 next_chunk
= self
.__chunks
[chunk
.end
+ 1]
453 while next
.get_line() <= chunk
.end
:
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
):
464 if not next
.ends_line():
465 next
.forward_to_line_end()
468 if not isinstance(chunk
, ResultChunk
):
469 chunk_start
, chunk_end
= iter, next
470 if chunk
== start_chunk
:
475 if chunk
== end_chunk
:
480 yield self
.get_slice(chunk_start
, chunk_end
)
484 if chunk
== end_chunk
or next_chunk
== None:
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)
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
):
520 nr_line
, offset
= nr_pos
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()
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):
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()):
574 if not (text
.startswith("\r") or text
.startswith("\n")):
578 is_pure_insert
= True
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
:
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
)
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
)
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
)
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
()
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
)
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
):
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
:
678 elif c
.end
>= chunk
.start
:
679 c
.end
= chunk
.start
- 1
681 if c
.start
>= chunk
.end
:
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
):
691 elif isinstance(chunk
, StatementChunk
):
694 def __find_statement_for_result(self
, result_chunk
):
695 line
= result_chunk
.start
- 1
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
]
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
725 elif isinstance(self
.__chunks
[i
], StatementChunk
) and self
.__chunks
[i
].start
== i
:
730 def __fixup_results(self
, state
, revalidate_iters
):
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
:
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
:
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
):
753 if not (move_before
or delete_after
or move_after
):
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
)
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
)
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+)
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
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:
837 if not end
.ends_line():
838 end
.forward_to_line_end()
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
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
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
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
:
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
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
()
929 print "After delete, chunks are", self
.__chunks
934 for chunk
in self
.iterate_chunks():
935 if isinstance(chunk
, StatementChunk
):
937 if chunk
.needs_compile
or (chunk
.needs_execute
and not have_error
):
938 old_result
= self
.__find
_result
(chunk
)
940 self
.__delete
_chunk
(old_result
)
942 if chunk
.needs_compile
:
945 if chunk
.error_message
!= None:
946 self
.insert_result(chunk
)
948 if chunk
.needs_execute
and not have_error
:
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:
960 self
.emit("chunk-status-changed", chunk
)
962 parent
= chunk
.statement
965 print "After calculate, chunks are", self
.__chunks
967 def __set_pair_location(self
, location
):
973 old_location
= self
.get_iter_at_mark(self
.__pair
_mark
)
974 self
.__have
_pair
= False
977 if not self
.__have
_pair
:
978 self
.__have
_pair
= True
979 self
.move_mark(self
.__pair
_mark
, location
)
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
)
988 self
.emit('pair-location-changed', old_location
, location
)
990 def get_pair_location(self
):
992 return self
.get_iter_at_mark(self
.__pair
_mark
)
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)
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)
1013 if location
.starts_line():
1014 self
.__set
_pair
_location
(None)
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)
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
):
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
1035 if mark
!= self
.get_insert() and mark
!= self
.get_selection_bound():
1038 self
.__calculate
_pair
_location
()
1040 def get_chunk(self
, line_index
):
1041 return self
.__chunks
[line_index
]
1044 self
.__undo
_stack
.undo()
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()
1056 def copy_as_doctests(self
, clipboard
):
1057 bounds
= self
.get_selection_bounds()
1059 start
, end
= self
.get_iter_at_mark(self
.get_insert())
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
1071 result
+= chunk_text
+ "\n"
1074 for line
in chunk_text
.split("\n"):
1075 if isinstance(chunk
, StatementChunk
) and not first
:
1076 result
+= "... " + line
+ "\n"
1078 result
+= ">>> " + line
+ "\n"
1081 clipboard
.set_text(result
)
1083 def __fontify_statement_lines(self
, chunk
, changed_lines
):
1084 iter = self
.get_iter_at_line(chunk
.start
)
1086 for l
in changed_lines
:
1092 self
.remove_all_tags(iter, end
)
1095 for token_type
, start_index
, end_index
, _
in chunk
.tokenized
.get_tokens(l
):
1096 tag
= self
.__fontify
_tags
[token_type
]
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
]
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)
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
)
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
):
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
:
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())
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
):
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")
1229 for chunk_text
in self
.iterate_text():
1233 os
.rename(tmpname
, filename
)
1236 self
.__set
_filename
_and
_modified
(filename
, False)
1242 if __name__
== '__main__':
1248 def compare(l1
, l2
):
1249 if len(l1
) != len(l2
):
1252 for i
in xrange(0, len(l1
)):
1256 if type(e1
) != type(e2
) or e1
.start
!= e2
.start
or e1
.end
!= e2
.end
:
1261 buffer = ShellBuffer(Notebook())
1263 def validate_nr_start():
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
))
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
)
1284 if end_offset
!= None:
1285 j
= buffer.get_iter_at_line_offset(end_line
, end_offset
)
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)
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)])
1312 # Turning a statement into a continuation line
1313 insert(0, 0, "1 \\\n+ 2\n")
1315 expect([S(0,1), B(2,2)])
1317 # Calculation resulting in result chunks
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
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
1330 insert(0, 0, "1\\\n + 2\\\n + 3")
1334 # Editing a line with an existing error chunk to fix the error
1337 insert(0, 0, "a\na=2")
1343 expect([S(0,0), R(1,1), S(2,2)])
1345 # Deleting an entire continuation line
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)])
1351 expect([S(0,1), B(2,2)])
1353 # Test an attempt to join a ResultChunk onto a previous chunk; should ignore
1356 insert(0, 0, "1\n");
1358 expect([S(0,0), R(1,1), B(2,2)])
1362 # Test an attempt to join a chunk onto a previous ResultChunk, should move
1363 # the ResultChunk and do the modification
1366 insert(0, 0, "1\n2\n");
1368 expect([S(0,0), R(1,1), S(2,2), R(3,3), B(4,4)])
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
1376 insert(0, 0, "1\n2");
1378 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
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
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
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
1396 expect_text("1\n\n2");
1397 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4)])
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
1406 insert(0, 0, "for i in range(0, 10): print i")
1408 expect([S(0, 0), R(1, 10)])
1410 # Test deleting a range containing both results and statements
1414 insert(0, 0, "1\n2\n3\n4\n")
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)])
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)])
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
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
1434 insert(0, 0, "12\n34")
1448 # Undoing insertion of a newline
1457 # Test the "pruning" behavior of modifications after undos
1464 buffer.redo() # does nothing
1468 # Test coalescing consecutive inserts
1476 # Test grouping of multiple undos by user actions
1480 buffer.begin_user_action()
1483 buffer.end_user_action()
1489 # Make sure that coalescing doesn't coalesce one user action with
1490 # only part of another
1494 buffer.begin_user_action()
1497 buffer.end_user_action()
1503 # Test an undo of an insert that caused insertion of result chunks
1507 expect([S(0,0), B(1,1)])
1509 expect([S(0,0), R(1,1), B(2,2)])
1513 expect([S(0,0), R(1,1), B(2,2)])
1516 # Tests of get_public_text()
1518 insert(0, 0, "12\n34\n56")
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
1535 SAVE_TEST
= """a = 1
1541 insert(0, 0, SAVE_TEST
)
1544 handle
, fname
= tempfile
.mkstemp(".txt", "shell_buffer")
1549 f
= open(fname
, "r")
1553 if saved
!= SAVE_TEST
:
1554 raise AssertionError("Got '%s', expected '%s'", saved
, SAVE_TEST
)
1559 expect([S(0,0), S(1,1), R(2,2), C(3,3), B(4,4), S(5,5)])