7 from notebook
import Notebook
8 from statement
import Statement
, ExecutionError
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" + str(e
.cause
)
101 self
.error_line
= e
.traceback
.tb_frame
.f_lineno
102 self
.error_offset
= None
105 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
108 self
.nr_start
= nr_start
111 return "BlankChunk(%d,%d)" % (self
.start
, self
.end
)
114 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
117 self
.nr_start
= nr_start
120 return "CommentChunk(%d,%d)" % (self
.start
, self
.end
)
123 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
126 self
.nr_start
= nr_start
129 return "ResultChunk(%d,%d)" % (self
.start
, self
.end
)
131 BLANK
= re
.compile(r
'^\s*$')
132 COMMENT
= re
.compile(r
'^\s*#')
133 CONTINUATION
= re
.compile(r
'^\s+')
135 class ResultChunkFixupState
:
138 class ShellBuffer(gtk
.TextBuffer
, Worksheet
):
140 'begin-user-action': 'override',
141 'end-user-action': 'override',
142 'insert-text': 'override',
143 'delete-range': 'override',
144 'chunk-status-changed': (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, (gobject
.TYPE_PYOBJECT
,)),
145 'add-custom-result': (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
)),
147 # It would be more GObject to make these properties, but we'll wait on that until
148 # decent property support lands:
150 # http://blogs.gnome.org/johan/2007/04/30/simplified-gobject-properties/
152 'filename-changed': (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
153 # Clumsy naming is because GtkTextBuffer already has a modified flag, but that would
154 # include changes to the results
155 'code-modified-changed': (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
158 def __init__(self
, notebook
):
159 gtk
.TextBuffer
.__init
__(self
)
160 Worksheet
.__init
__(self
, notebook
)
162 self
.__red
_tag
= self
.create_tag(foreground
="red")
163 self
.__result
_tag
= self
.create_tag(family
="monospace", style
="italic", wrap_mode
=gtk
.WRAP_WORD
, editable
=False)
164 # Order here is significant ... we want the recompute tag to have higher priority, so
166 self
.__error
_tag
= self
.create_tag(foreground
="#aa0000")
167 self
.__recompute
_tag
= self
.create_tag(foreground
="#888888")
168 self
.__comment
_tag
= self
.create_tag(foreground
="#3f7f5f")
170 punctuation_tag
= None
172 self
.__fontify
_tags
= {
173 tokenize
.TOKEN_KEYWORD
: self
.create_tag(foreground
="#7f0055", weight
=600),
174 tokenize
.TOKEN_NAME
: None,
175 tokenize
.TOKEN_COMMENT
: self
.__comment
_tag
,
176 tokenize
.TOKEN_STRING
: self
.create_tag(foreground
="#00aa00"),
177 tokenize
.TOKEN_PUNCTUATION
: punctuation_tag
,
178 tokenize
.TOKEN_CONTINUATION
: punctuation_tag
,
179 tokenize
.TOKEN_LPAREN
: punctuation_tag
,
180 tokenize
.TOKEN_RPAREN
: punctuation_tag
,
181 tokenize
.TOKEN_LSQB
: punctuation_tag
,
182 tokenize
.TOKEN_RSQB
: punctuation_tag
,
183 tokenize
.TOKEN_LBRACE
: punctuation_tag
,
184 tokenize
.TOKEN_RBRACE
: punctuation_tag
,
185 tokenize
.TOKEN_BACKQUOTE
: punctuation_tag
,
186 tokenize
.TOKEN_NUMBER
: None,
187 tokenize
.TOKEN_JUNK
: self
.create_tag(underline
="error"),
191 self
.__chunks
= [BlankChunk(0,0, 0)]
192 self
.__modifying
_results
= False
193 self
.__applying
_undo
= False
194 self
.__user
_action
_count
= 0
196 self
.__undo
_stack
= UndoStack(self
)
199 self
.code_modified
= False
201 def __compute_nr_start(self
, chunk
):
205 chunk_before
= self
.__chunks
[chunk
.start
- 1]
206 if isinstance(chunk_before
, ResultChunk
):
207 chunk
.nr_start
= chunk_before
.nr_start
209 chunk
.nr_start
= chunk_before
.nr_start
+ (1 + chunk_before
.end
- chunk_before
.start
)
211 def __assign_lines(self
, chunk_start
, lines
, statement_end
):
214 if statement_end
>= chunk_start
:
215 def notnull(l
): return l
!= None
216 chunk_lines
= filter(notnull
, lines
[0:statement_end
+ 1 - chunk_start
])
219 for i
in xrange(chunk_start
, statement_end
+ 1):
220 if isinstance(self
.__chunks
[i
], StatementChunk
):
221 old_statement
= self
.__chunks
[i
]
224 if old_statement
!= None:
225 # An old statement can only be turned into *one* new statement; this
226 # prevents us getting fooled if we split a statement
227 for i
in xrange(old_statement
.start
, old_statement
.end
+ 1):
228 self
.__chunks
[i
] = None
230 chunk
= old_statement
231 changed_lines
= chunk
.set_lines(chunk_lines
)
232 changed
= changed_lines
!= []
234 # If we moved the statement with respect to the buffer, then the we
235 # need to refontify, even if the old statement didn't change
236 if old_statement
.start
!= chunk_start
:
237 changed_lines
= range(0, 1 + statement_end
- chunk_start
)
239 chunk
= StatementChunk()
240 changed_lines
= chunk
.set_lines(chunk_lines
)
244 changed_chunks
.append(chunk
)
246 chunk
.start
= chunk_start
247 chunk
.end
= statement_end
248 self
.__compute
_nr
_start
(chunk
)
249 self
.__fontify
_statement
_lines
(chunk
, changed_lines
)
251 for i
in xrange(chunk_start
, statement_end
+ 1):
252 self
.__chunks
[i
] = chunk
253 self
.__lines
[i
] = lines
[i
- chunk_start
]
255 for i
in xrange(statement_end
+ 1, chunk_start
+ len(lines
)):
256 line
= lines
[i
- chunk_start
]
259 chunk
= self
.__chunks
[i
- 1]
264 # a ResultChunk Must be in the before-start portion, nothing needs doing
266 elif BLANK
.match(line
):
267 if not isinstance(chunk
, BlankChunk
):
270 self
.__compute
_nr
_start
(chunk
)
272 self
.__chunks
[i
] = chunk
273 self
.__lines
[i
] = lines
[i
- chunk_start
]
274 elif COMMENT
.match(line
):
275 if not isinstance(chunk
, CommentChunk
):
276 chunk
= CommentChunk()
278 self
.__compute
_nr
_start
(chunk
)
280 self
.__chunks
[i
] = chunk
281 self
.__lines
[i
] = lines
[i
- chunk_start
]
282 # This is O(n^2) inefficient
283 self
.__apply
_tag
_to
_chunk
(self
.__comment
_tag
, chunk
)
285 return changed_chunks
287 def __mark_rest_for_execute(self
, start_line
):
288 for chunk
in self
.iterate_chunks(start_line
):
289 if isinstance(chunk
, StatementChunk
):
290 chunk
.mark_for_execute()
292 result
= self
.__find
_result
(chunk
)
294 self
.__apply
_tag
_to
_chunk
(self
.__recompute
_tag
, result
)
296 self
.emit("chunk-status-changed", chunk
)
298 self
.emit("chunk-status-changed", result
)
300 def __rescan(self
, start_line
, end_line
, entire_statements_deleted
=False):
301 rescan_start
= start_line
302 while rescan_start
> 0:
303 if rescan_start
< start_line
:
304 new_text
= old_text
= self
.__lines
[rescan_start
]
306 old_text
= self
.__lines
[rescan_start
]
307 i
= self
.get_iter_at_line(rescan_start
)
309 if not i_end
.ends_line():
310 i_end
.forward_to_line_end()
311 new_text
= self
.get_slice(i
, i_end
)
313 if old_text
== None or BLANK
.match(old_text
) or COMMENT
.match(old_text
) or CONTINUATION
.match(old_text
) or \
314 new_text
== None or BLANK
.match(new_text
) or COMMENT
.match(new_text
) or CONTINUATION
.match(new_text
):
319 # If previous contents of the modified range ended within a statement, then we need to rescan all of it;
320 # since we may have already deleted all of the statement lines within the modified range, we detect
321 # this case by seeing if the line *after* our range is a continuation line.
322 rescan_end
= end_line
323 while rescan_end
+ 1 < len(self
.__chunks
):
324 if isinstance(self
.__chunks
[rescan_end
+ 1], StatementChunk
) and self
.__chunks
[rescan_end
+ 1].start
!= rescan_end
+ 1:
329 chunk_start
= rescan_start
330 statement_end
= rescan_start
- 1
334 i
= self
.get_iter_at_line(rescan_start
)
338 for line
in xrange(rescan_start
, rescan_end
+ 1):
339 if line
< start_line
:
340 line_text
= self
.__lines
[line
]
343 if not i_end
.ends_line():
344 i_end
.forward_to_line_end()
345 line_text
= self
.get_slice(i
, i_end
)
347 if line_text
== None:
348 chunk_lines
.append(line_text
)
349 elif BLANK
.match(line_text
):
350 chunk_lines
.append(line_text
)
351 elif COMMENT
.match(line_text
):
352 chunk_lines
.append(line_text
)
353 elif CONTINUATION
.match(line_text
):
354 chunk_lines
.append(line_text
)
357 changed_chunks
.extend(self
.__assign
_lines
(chunk_start
, chunk_lines
, statement_end
))
360 chunk_lines
= [line_text
]
364 changed_chunks
.extend(self
.__assign
_lines
(chunk_start
, chunk_lines
, statement_end
))
365 if len(changed_chunks
) > 0:
366 # The the chunks in changed_chunks are already marked as needing recompilation; we
367 # need to emit signals and also mark those chunks and all subsequent chunks as
368 # needing reexecution
369 first_changed_line
= changed_chunks
[0].start
370 for chunk
in changed_chunks
:
371 if chunk
.start
< first_changed_line
:
372 first_changed_line
= chunk
.start
374 self
.__mark
_rest
_for
_execute
(first_changed_line
)
375 elif entire_statements_deleted
:
376 # If the user deleted entire statements we need to mark subsequent chunks
377 # as needing compilation even if all the remaining statements remained unchanged
378 self
.__mark
_rest
_for
_execute
(end_line
+ 1)
380 def iterate_chunks(self
, start_line
=0, end_line
=None):
381 if end_line
== None or end_line
>= len(self
.__chunks
):
382 end_line
= len(self
.__chunks
) - 1
383 if start_line
>= len(self
.__chunks
) or end_line
< start_line
:
386 chunk
= self
.__chunks
[start_line
]
387 while chunk
== None and start_line
< end_line
:
389 chunk
= self
.__chunks
[start_line
]
394 last_chunk
= self
.__chunks
[end_line
]
395 while last_chunk
== None:
397 last_chunk
= self
.__chunks
[end_line
]
401 if chunk
== last_chunk
:
404 chunk
= self
.__chunks
[line
]
407 chunk
= self
.__chunks
[line
]
409 def iterate_text(self
):
410 iter = self
.get_start_iter()
412 chunk
= self
.__chunks
[0]
415 next_line
= chunk
.end
+ 1
416 if next_line
< len(self
.__chunks
):
417 next_chunk
= self
.__chunks
[chunk
.end
+ 1]
420 while next
.get_line() <= chunk
.end
:
425 next
.forward_to_end()
427 # Special case .... if the last chunk is a ResultChunk, then we don't
428 # want to include the new line from the previous line
429 if isinstance(next_chunk
, ResultChunk
) and next_chunk
.end
+ 1 == len(self
.__chunks
):
431 if not next
.ends_line():
432 next
.forward_to_line_end()
435 if not isinstance(chunk
, ResultChunk
):
436 chunk_text
= self
.get_slice(iter, next
)
443 def do_begin_user_action(self
):
444 self
.__user
_action
_count
+= 1
445 self
.__undo
_stack
.begin_user_action()
447 def do_end_user_action(self
):
448 self
.__user
_action
_count
-= 1
449 self
.__undo
_stack
.end_user_action()
451 def __compute_nr_pos_from_chunk_offset(self
, chunk
, line
, offset
):
452 if isinstance(chunk
, ResultChunk
):
453 prev_chunk
= self
.__chunks
[chunk
.start
- 1]
454 iter = self
.get_iter_at_line(prev_chunk
.end
)
455 if not iter.ends_line():
456 iter.forward_to_line_end()
457 return (prev_chunk
.end
- prev_chunk
.start
+ prev_chunk
.nr_start
, iter.get_line_offset(), 1)
459 return (line
- chunk
.start
+ chunk
.nr_start
, offset
)
461 def __compute_nr_pos_from_iter(self
, iter):
462 line
= iter.get_line()
463 chunk
= self
.__chunks
[line
]
464 return self
.__compute
_nr
_pos
_from
_chunk
_offset
(chunk
, line
, iter.get_line_offset())
466 def __compute_nr_pos_from_line_offset(self
, line
, offset
):
467 return self
.__compute
_nr
_pos
_from
_chunk
_offset
(self
.__chunks
[line
], line
, offset
)
469 def _get_iter_at_nr_pos(self
, nr_pos
):
471 nr_line
, offset
= nr_pos
474 nr_line
, offset
, in_result
= nr_pos
476 for chunk
in self
.iterate_chunks():
477 if not isinstance(chunk
, ResultChunk
) and chunk
.nr_start
+ (chunk
.end
- chunk
.start
) >= nr_line
:
478 line
= chunk
.start
+ nr_line
- chunk
.nr_start
479 iter = self
.get_iter_at_line(line
)
480 iter.set_line_offset(offset
)
482 if in_result
and chunk
.end
+ 1 < len(self
.__chunks
):
483 next_chunk
= self
.__chunks
[chunk
.end
+ 1]
484 if isinstance(next_chunk
, ResultChunk
):
485 iter = self
.get_iter_at_line(next_chunk
.end
)
486 if not iter.ends_line():
487 iter.forward_to_line_end()
491 raise AssertionError("nr_pos pointed outside buffer")
494 def __insert_blank_line_after(self
, chunk_before
, location
, separator
):
495 start_pos
= self
.__compute
_nr
_pos
_from
_iter
(location
)
497 self
.__modifying
_results
= True
498 gtk
.TextBuffer
.do_insert_text(self
, location
, separator
, len(separator
))
499 self
.__modifying
_results
= False
501 new_chunk
= BlankChunk(chunk_before
.end
+ 1, chunk_before
.end
+ 1, chunk_before
.nr_start
)
502 self
.__chunks
[chunk_before
.end
+ 1:chunk_before
.end
+ 1] = [new_chunk
]
503 self
.__lines
[chunk_before
.end
+ 1:chunk_before
.end
+ 1] = [""]
505 for chunk
in self
.iterate_chunks(new_chunk
.end
+ 1):
510 end_pos
= self
.__compute
_nr
_pos
_from
_iter
(location
)
511 self
.__undo
_stack
.append_op(InsertOp(start_pos
, end_pos
, separator
))
513 def do_insert_text(self
, location
, text
, text_len
):
514 start_line
= location
.get_line()
515 is_pure_insert
= False
516 if self
.__user
_action
_count
> 0:
517 current_chunk
= self
.__chunks
[start_line
]
518 if isinstance(current_chunk
, ResultChunk
):
519 # The only thing that's valid to do with a ResultChunk is insert
520 # a newline at the end to get another line after it
521 if not (start_line
== current_chunk
.end
and location
.ends_line()):
524 if not (text
.startswith("\r") or text
.startswith("\n")):
528 is_pure_insert
= True
531 if not self
.__modifying
_results
:
532 print "Inserting '%s' at %s" % (text
, (location
.get_line(), location
.get_line_offset()))
534 start_pos
= self
.__compute
_nr
_pos
_from
_iter
(location
)
536 gtk
.TextBuffer
.do_insert_text(self
, location
, text
, text_len
)
537 end_line
= location
.get_line()
538 end_offset
= location
.get_line_offset()
540 if self
.__modifying
_results
:
543 if self
.__user
_action
_count
> 0:
544 self
.__set
_modified
(True)
546 result_fixup_state
= self
.__get
_result
_fixup
_state
(start_line
, start_line
)
549 self
.__chunks
[start_line
:start_line
] = [None for i
in xrange(start_line
, end_line
+ 1)]
550 self
.__lines
[start_line
:start_line
] = [None for i
in xrange(start_line
, end_line
+ 1)]
552 for chunk
in self
.iterate_chunks(end_line
+ 1):
553 if chunk
.start
>= start_line
:
554 chunk
.start
+= (1 + end_line
- start_line
)
555 chunk
.nr_start
+= (1 + end_line
- start_line
)
556 if chunk
.end
>= start_line
:
557 chunk
.end
+= (1 + end_line
- start_line
)
559 self
.__chunks
[start_line
+ 1:start_line
+ 1] = [None for i
in xrange(start_line
, end_line
)]
560 self
.__lines
[start_line
+ 1:start_line
+ 1] = [None for i
in xrange(start_line
, end_line
)]
562 for chunk
in self
.iterate_chunks(start_line
):
563 if chunk
.start
> start_line
:
564 chunk
.start
+= (end_line
- start_line
)
565 chunk
.nr_start
+= (end_line
- start_line
)
566 if chunk
.end
> start_line
:
567 chunk
.end
+= (end_line
- start_line
)
569 self
.__rescan
(start_line
, end_line
)
571 end_pos
= self
.__compute
_nr
_pos
_from
_line
_offset
(end_line
, end_offset
)
572 self
.__undo
_stack
.append_op(InsertOp(start_pos
, end_pos
, text
[0:text_len
]))
574 self
.__fixup
_results
(result_fixup_state
, [location
])
577 print "After insert, chunks are", self
.__chunks
579 def __delete_chunk(self
, chunk
):
580 self
.__modifying
_results
= True
582 i_start
= self
.get_iter_at_line(chunk
.start
)
583 i_end
= self
.get_iter_at_line(chunk
.end
)
585 if i_end
.get_line() == chunk
.end
:
586 # Last line of buffer, need to delete the chunk and not
587 # leave a trailing newline
588 if not i_end
.ends_line():
589 i_end
.forward_to_line_end()
590 i_start
.backward_line()
591 if not i_start
.ends_line():
592 i_start
.forward_to_line_end()
593 self
.delete(i_start
, i_end
)
595 self
.__chunks
[chunk
.start
:chunk
.end
+ 1] = []
596 self
.__lines
[chunk
.start
:chunk
.end
+ 1] = []
598 n_deleted
= chunk
.end
+ 1 - chunk
.start
599 if isinstance(chunk
, ResultChunk
):
602 n_deleted
= n_nr_deleted
604 # Overlapping chunks can occur temporarily when inserting
605 # or deleting text merges two adjacent statements with a ResultChunk in between, so iterate
606 # all chunks, not just the ones after the deleted chunk
607 for c
in self
.iterate_chunks():
608 if c
.end
>= chunk
.end
:
610 elif c
.end
>= chunk
.start
:
611 c
.end
= chunk
.start
- 1
613 if c
.start
>= chunk
.end
:
615 c
.nr_start
-= n_nr_deleted
617 self
.__modifying
_results
= False
619 def __find_result(self
, statement
):
620 for chunk
in self
.iterate_chunks(statement
.end
+ 1):
621 if isinstance(chunk
, ResultChunk
):
623 elif isinstance(chunk
, StatementChunk
):
626 def __find_statement_for_result(self
, result_chunk
):
627 line
= result_chunk
.start
- 1
629 if isinstance(self
.__chunks
[line
], StatementChunk
):
630 return self
.__chunks
[line
]
631 raise AssertionError("Result with no corresponding statement")
633 def __get_result_fixup_state(self
, first_modified_line
, last_modified_line
):
634 state
= ResultChunkFixupState()
636 state
.statement_before
= None
637 state
.result_before
= None
638 for i
in xrange(first_modified_line
- 1, -1, -1):
639 if isinstance(self
.__chunks
[i
], ResultChunk
):
640 state
.result_before
= self
.__chunks
[i
]
641 elif isinstance(self
.__chunks
[i
], StatementChunk
):
642 if state
.result_before
!= None:
643 state
.statement_before
= self
.__chunks
[i
]
646 state
.statement_after
= None
647 state
.result_after
= None
649 for i
in xrange(last_modified_line
+ 1, len(self
.__chunks
)):
650 if isinstance(self
.__chunks
[i
], ResultChunk
):
651 state
.result_after
= self
.__chunks
[i
]
652 for j
in xrange(i
- 1, -1, -1):
653 if isinstance(self
.__chunks
[j
], StatementChunk
):
654 state
.statement_after
= self
.__chunks
[j
]
655 assert state
.statement_after
.results
!= None or state
.statement_after
.error_message
!= None
657 elif isinstance(self
.__chunks
[i
], StatementChunk
) and self
.__chunks
[i
].start
== i
:
662 def __fixup_results(self
, state
, revalidate_iters
):
667 if state
.result_before
!= None:
668 # If lines were added into the StatementChunk that produced the ResultChunk above the edited segment,
669 # then the ResultChunk needs to be moved after the newly inserted lines
670 if state
.statement_before
.end
> state
.result_before
.start
:
673 if state
.result_after
!= None:
674 # If the StatementChunk that produced the ResultChunk after the edited segment was deleted, then the
675 # ResultChunk needs to be deleted as well
676 if self
.__chunks
[state
.statement_after
.start
] != state
.statement_after
:
679 # If another StatementChunk was inserted between the StatementChunk and the ResultChunk, then we
680 # need to move the ResultChunk above that statement
681 for i
in xrange(state
.statement_after
.end
+ 1, state
.result_after
.start
):
682 if self
.__chunks
[i
] != state
.statement_after
and isinstance(self
.__chunks
[i
], StatementChunk
):
685 if not (move_before
or delete_after
or move_after
):
689 print "Result fixups: move_before=%s, delete_after=%s, move_after=%s" % (move_before
, delete_after
, move_after
)
691 revalidate
= map(lambda iter: (iter, self
.create_mark(None, iter, True)), revalidate_iters
)
694 self
.__delete
_chunk
(state
.result_before
)
695 self
.insert_result(state
.statement_before
)
697 if delete_after
or move_after
:
698 self
.__delete
_chunk
(state
.result_after
)
700 self
.insert_result(state
.statement_after
)
702 for iter, mark
in revalidate
:
703 _copy_iter(iter, self
.get_iter_at_mark(mark
))
704 self
.delete_mark(mark
)
706 def do_delete_range(self
, start
, end
):
708 # Note that there is a bug in GTK+ versions prior to 2.12.2, where it doesn't work
709 # if a ::delete-range handler deletes stuff outside it's requested range. (No crash,
710 # gtk_text_buffer_delete_interactive() just leaves some editable text undeleleted.)
711 # See: http://bugzilla.gnome.org/show_bug.cgi?id=491207
713 # The only workaround I can think of right now would be to stop using not-editable
714 # tags on results, and implement the editability ourselves in ::insert-text
715 # and ::delete-range, but a) that's a lot of work to rewrite that way b) it will make
716 # the text view give worse feedback. So, I'm just leaving the problem for now,
717 # (and have committed the fix to GTK+)
720 if not self
.__modifying
_results
:
721 print "Request to delete range %s" % (((start
.get_line(), start
.get_line_offset()), (end
.get_line(), end
.get_line_offset())),)
722 start_line
= start
.get_line()
723 end_line
= end
.get_line()
725 restore_result_statement
= None
727 # Prevent the user from doing deletes that would merge a ResultChunk chunk with another chunk
728 if self
.__user
_action
_count
> 0 and not self
.__modifying
_results
:
729 if start
.ends_line() and isinstance(self
.__chunks
[start_line
], ResultChunk
):
730 # Merging another chunk onto the end of a ResultChunk; e.g., hitting delete at the
731 # start of a line with a ResultChunk before it. We don't want to actually ignore this,
732 # since otherwise if you split a line, you can't join it back up, instead we actually
733 # have to do what the user wanted to do ... join the two lines.
735 # We delete the result chunk, and if everything still looks sane at the very end,
736 # we insert it back; this is not unified with the __fixup_results() codepaths, since
737 # A) There's no insert analogue B) That's complicated enough as it is. But if we
738 # have problems, we might want to reconsider whether there is some unified way to
739 # do both. Maybe we should just delete all possibly affected ResultChunks and add
740 # them all back at the end?
742 result_chunk
= self
.__chunks
[start_line
]
743 restore_result_statement
= self
.__find
_statement
_for
_result
(result_chunk
)
744 end_offset
= end
.get_line_offset()
745 self
.__modifying
_results
= True
746 self
.__delete
_chunk
(result_chunk
)
747 self
.__modifying
_results
= False
748 start_line
-= 1 + result_chunk
.end
- result_chunk
.start
749 end_line
-= 1 + result_chunk
.end
- result_chunk
.start
750 _copy_iter(start
, self
.get_iter_at_line(start_line
))
751 if not start
.ends_line():
752 start
.forward_to_line_end()
753 _copy_iter(end
, self
.get_iter_at_line_offset(end_line
, end_offset
))
755 if end
.starts_line() and not start
.starts_line() and isinstance(self
.__chunks
[end_line
], ResultChunk
):
756 # Merging a ResultChunk onto the end of another chunk; just ignore this; we do have
757 # have to be careful to avoid leaving end pointing to the same place as start, since
758 # we'll then go into an infinite loop
761 new_end
.backward_line()
762 if not new_end
.ends_line():
763 new_end
.forward_to_line_end()
765 if start
.compare(new_end
) == 0:
769 if not new_end
.ends_line():
770 new_end
.forward_to_line_end()
773 if start
.starts_line() and end
.starts_line():
774 (first_deleted_line
, last_deleted_line
) = (start_line
, end_line
- 1)
775 (new_start
, new_end
) = (start_line
, start_line
- 1) # empty
776 last_modified_line
= end_line
- 1
777 elif start
.starts_line():
778 if start_line
== end_line
:
779 (first_deleted_line
, last_deleted_line
) = (start_line
, start_line
- 1) # empty
780 (new_start
, new_end
) = (start_line
, start_line
)
781 last_modified_line
= start_line
783 (first_deleted_line
, last_deleted_line
) = (start_line
, end_line
- 1)
784 (new_start
, new_end
) = (start_line
, start_line
)
785 last_modified_line
= end_line
787 (first_deleted_line
, last_deleted_line
) = (start_line
+ 1, end_line
)
788 (new_start
, new_end
) = (start_line
, start_line
)
789 last_modified_line
= end_line
792 if not self
.__modifying
_results
:
793 print "Deleting range %s" % (((start
.get_line(), start
.get_line_offset()), (end
.get_line(), end
.get_line_offset())),)
794 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
)
796 start_pos
= self
.__compute
_nr
_pos
_from
_iter
(start
)
797 end_pos
= self
.__compute
_nr
_pos
_from
_iter
(end
)
798 deleted_text
= self
.get_slice(start
, end
)
799 gtk
.TextBuffer
.do_delete_range(self
, start
, end
)
801 if self
.__modifying
_results
:
804 if self
.__user
_action
_count
> 0:
805 self
.__set
_modified
(True)
807 self
.__undo
_stack
.append_op(DeleteOp(start_pos
, end_pos
, deleted_text
))
809 result_fixup_state
= self
.__get
_result
_fixup
_state
(new_start
, last_modified_line
)
811 entire_statements_deleted
= False
813 for chunk
in self
.iterate_chunks(first_deleted_line
, last_deleted_line
):
814 if isinstance(chunk
, StatementChunk
) and chunk
.start
>= first_deleted_line
and chunk
.end
<= last_deleted_line
:
815 entire_statements_deleted
= True
817 if not isinstance(chunk
, ResultChunk
):
818 n_nr_deleted
+= 1 + min(last_deleted_line
, chunk
.end
) - max(first_deleted_line
, chunk
.start
)
820 n_deleted
= 1 + last_deleted_line
- first_deleted_line
821 self
.__chunks
[first_deleted_line
:last_deleted_line
+ 1] = []
822 self
.__lines
[first_deleted_line
:last_deleted_line
+ 1] = []
824 for chunk
in self
.iterate_chunks():
825 if chunk
.end
>= last_deleted_line
:
826 chunk
.end
-= n_deleted
;
827 elif chunk
.end
>= first_deleted_line
:
828 chunk
.end
= first_deleted_line
- 1
830 if chunk
.start
>= last_deleted_line
:
831 chunk
.start
-= n_deleted
832 chunk
.nr_start
-= n_nr_deleted
834 self
.__rescan
(new_start
, new_end
, entire_statements_deleted
=entire_statements_deleted
)
836 self
.__fixup
_results
(result_fixup_state
, [start
, end
])
838 if restore_result_statement
!= None and \
839 self
.__chunks
[restore_result_statement
.start
] == restore_result_statement
and \
840 self
.__find
_result
(restore_result_statement
) == None:
841 start_mark
= self
.create_mark(None, start
, True)
842 end_mark
= self
.create_mark(None, end
, True)
843 result_chunk
= self
.insert_result(restore_result_statement
)
844 _copy_iter(start
, self
.get_iter_at_mark(start_mark
))
845 self
.delete_mark(start_mark
)
846 _copy_iter(end
, self
.get_iter_at_mark(end_mark
))
847 self
.delete_mark(end_mark
)
849 # If the cursor ended up in or after the restored result chunk,
850 # we need to move it before
851 insert
= self
.get_iter_at_mark(self
.get_insert())
852 if insert
.get_line() >= result_chunk
.start
:
853 insert
.set_line(result_chunk
.start
- 1)
854 if not insert
.ends_line():
855 insert
.forward_to_line_end()
856 self
.place_cursor(insert
)
859 print "After delete, chunks are", self
.__chunks
864 for chunk
in self
.iterate_chunks():
865 if isinstance(chunk
, StatementChunk
):
867 if chunk
.needs_compile
or (chunk
.needs_execute
and not have_error
):
868 old_result
= self
.__find
_result
(chunk
)
870 self
.__delete
_chunk
(old_result
)
872 if chunk
.needs_compile
:
875 if chunk
.error_message
!= None:
876 self
.insert_result(chunk
)
878 if chunk
.needs_execute
and not have_error
:
880 chunk
.execute(parent
)
881 if chunk
.error_message
!= None:
882 self
.insert_result(chunk
)
883 elif len(chunk
.results
) > 0:
884 self
.insert_result(chunk
)
886 if chunk
.error_message
!= None:
890 self
.emit("chunk-status-changed", chunk
)
892 parent
= chunk
.statement
895 print "After calculate, chunks are", self
.__chunks
897 def get_chunk(self
, line_index
):
898 return self
.__chunks
[line_index
]
901 self
.__undo
_stack
.undo()
904 self
.__undo
_stack
.redo()
906 def __get_chunk_bounds(self
, chunk
):
907 start
= self
.get_iter_at_line(chunk
.start
)
908 end
= self
.get_iter_at_line(chunk
.end
)
909 if not end
.ends_line():
910 end
.forward_to_line_end()
913 def __fontify_statement_lines(self
, chunk
, changed_lines
):
914 iter = self
.get_iter_at_line(chunk
.start
)
916 for l
in changed_lines
:
922 self
.remove_all_tags(iter, end
)
925 for token_type
, start_index
, end_index
in chunk
.tokenized
.get_tokens(l
):
926 tag
= self
.__fontify
_tags
[token_type
]
928 iter.set_line_index(start_index
)
929 end
.set_line_index(end_index
)
930 self
.apply_tag(tag
, iter, end
)
932 def __apply_tag_to_chunk(self
, tag
, chunk
):
933 start
, end
= self
.__get
_chunk
_bounds
(chunk
)
934 self
.apply_tag(tag
, start
, end
)
936 def __remove_tag_from_chunk(self
, tag
, chunk
):
937 start
, end
= self
.__get
_chunk
_bounds
(chunk
)
938 self
.remove_tag(tag
, start
, end
)
940 def insert_result(self
, chunk
):
941 self
.__modifying
_results
= True
942 location
= self
.get_iter_at_line(chunk
.end
)
943 if not location
.ends_line():
944 location
.forward_to_line_end()
946 if chunk
.error_message
:
947 results
= [ chunk
.error_message
]
949 results
= chunk
.results
951 for result
in results
:
952 if isinstance(result
, basestring
):
953 self
.insert(location
, "\n" + result
)
954 elif isinstance(result
, CustomResult
):
955 self
.insert(location
, "\n")
956 anchor
= self
.create_child_anchor(location
)
957 self
.emit("add-custom-result", result
, anchor
)
959 self
.__modifying
_results
= False
960 n_inserted
= location
.get_line() - chunk
.end
962 result_chunk
= ResultChunk(chunk
.end
+ 1, chunk
.end
+ n_inserted
)
963 self
.__compute
_nr
_start
(result_chunk
)
964 self
.__chunks
[chunk
.end
+ 1:chunk
.end
+ 1] = [result_chunk
for i
in xrange(0, n_inserted
)]
965 self
.__lines
[chunk
.end
+ 1:chunk
.end
+ 1] = [None for i
in xrange(0, n_inserted
)]
967 self
.__apply
_tag
_to
_chunk
(self
.__result
_tag
, result_chunk
)
969 if chunk
.error_message
:
970 self
.__apply
_tag
_to
_chunk
(self
.__error
_tag
, result_chunk
)
972 for chunk
in self
.iterate_chunks(result_chunk
.end
+ 1):
973 chunk
.start
+= n_inserted
974 chunk
.end
+= n_inserted
978 def __set_filename_and_modified(self
, filename
, modified
):
979 filename_changed
= filename
!= self
.filename
980 modified_changed
= modified
!= self
.code_modified
982 if not (filename_changed
or modified_changed
):
985 self
.filename
= filename
986 self
.code_modified
= modified
989 self
.emit('filename-changed')
992 self
.emit('code-modified-changed')
994 def __set_modified(self
, modified
):
995 if modified
== self
.code_modified
:
998 self
.code_modified
= modified
999 self
.emit('code-modified-changed')
1001 def __do_clear(self
):
1002 # This is actually working pretty much coincidentally, since the Delete
1003 # code wasn't really written with non-interactive deletes in mind, and
1004 # when there are ResultChunk present, a non-interactive delete will
1005 # use ranges including them. But the logic happens to work out.
1007 self
.delete(self
.get_start_iter(), self
.get_end_iter())
1011 self
.__set
_filename
_and
_modified
(None, False)
1013 # This prevents redoing New, but we need some more work to enable that
1014 self
.__undo
_stack
.clear()
1016 def load(self
, filename
):
1022 self
.__set
_filename
_and
_modified
(filename
, False)
1023 self
.insert(self
.get_start_iter(), text
)
1024 self
.__undo
_stack
.clear()
1026 def save(self
, filename
=None):
1027 if filename
== None:
1028 if self
.filename
== None:
1029 raise ValueError("No current or specified filename")
1031 filename
= self
.filename
1033 # TODO: The atomic-save implementation here is Unix-specific and won't work on Windows
1034 tmpname
= filename
+ ".tmp"
1036 # We use binary mode, since we don't want to munge line endings to the system default
1037 # on a load-save cycle
1038 f
= open(tmpname
, "wb")
1042 for chunk_text
in self
.iterate_text():
1046 os
.rename(tmpname
, filename
)
1049 self
.__set
_filename
_and
_modified
(filename
, False)
1055 if __name__
== '__main__':
1061 def compare(l1
, l2
):
1062 if len(l1
) != len(l2
):
1065 for i
in xrange(0, len(l1
)):
1069 if type(e1
) != type(e2
) or e1
.start
!= e2
.start
or e1
.end
!= e2
.end
:
1074 buffer = ShellBuffer(Notebook())
1076 def validate_nr_start():
1078 for chunk
in buffer.iterate_chunks():
1079 if chunk
.nr_start
!= n_nr
:
1080 raise AssertionError("nr_start for chunk %s should have been %d but is %d" % (chunk
, n_nr
, chunk
.nr_start
))
1081 assert(chunk
.nr_start
== n_nr
)
1082 if not isinstance(chunk
, ResultChunk
):
1083 n_nr
+= 1 + chunk
.end
- chunk
.start
1085 def expect(expected
):
1086 chunks
= [ x
for x
in buffer.iterate_chunks() ]
1087 if not compare(chunks
, expected
):
1088 raise AssertionError("\nGot:\n %s\nExpected:\n %s" % (chunks
, expected
))
1091 def expect_text(expected
):
1093 for chunk_text
in buffer.iterate_text():
1096 if (text
!= expected
):
1097 raise AssertionError("\nGot:\n '%s'\nExpected:\n '%s'" % (text
, expected
))
1099 def insert(line
, offset
, text
):
1100 i
= buffer.get_iter_at_line(line
)
1101 i
.set_line_offset(offset
)
1102 buffer.insert_interactive(i
, text
, True)
1104 def delete(start_line
, start_offset
, end_line
, end_offset
):
1105 i
= buffer.get_iter_at_line(start_line
)
1106 i
.set_line_offset(start_offset
)
1107 j
= buffer.get_iter_at_line(end_line
)
1108 j
.set_line_offset(end_offset
)
1109 buffer.delete_interactive(i
, j
, True)
1115 insert(0, 0, "1\n\n#2\ndef a():\n 3")
1116 expect([S(0,0), B(1,1), C(2,2), S(3,4)])
1121 # Turning a statement into a continuation line
1122 insert(0, 0, "1 \\\n+ 2\n")
1124 expect([S(0,1), B(2,2)])
1126 # Calculation resulting in result chunks
1129 expect([S(0,1), R(2,2), S(3,3), R(4,4), B(5,5)])
1131 # Check that splitting a statement with a delete results in the
1132 # result chunk being moved to the last line of the first half
1134 expect([S(0,0), R(1,1), S(2,2), S(3,3), R(4,4), B(5,5)])
1136 # Editing a continuation line, while leaving it a continuation
1139 insert(0, 0, "1\\\n + 2\\\n + 3")
1143 # Editing a line with an existing error chunk to fix the error
1146 insert(0, 0, "a\na=2")
1152 expect([S(0,0), R(1,1), S(2,2)])
1154 # Deleting an entire continuation line
1157 insert(0, 0, "for i in (1,2):\n print i\n print i + 1\n")
1158 expect([S(0,2), B(3,3)])
1160 expect([S(0,1), B(2,2)])
1162 # Test an attempt to join a ResultChunk onto a previous chunk; should ignore
1165 insert(0, 0, "1\n");
1167 expect([S(0,0), R(1,1), B(2,2)])
1171 # Test an attempt to join a chunk onto a previous ResultChunk, should move
1172 # the ResultChunk and do the modification
1175 insert(0, 0, "1\n2\n");
1177 expect([S(0,0), R(1,1), S(2,2), R(3,3), B(4,4)])
1179 expect([S(0,0), R(1,1), B(2,2)])
1180 expect_text("12\n");
1182 # Test inserting random text inside a result chunk, should ignore
1185 insert(0, 0, "1\n2");
1187 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
1189 expect_text("1\n2");
1190 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
1192 # Test inserting a newline at the end of a result chunk, should create
1195 expect_text("1\n\n2");
1196 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4)])
1198 # Same, at the end of the buffer
1200 expect_text("1\n\n2\n");
1201 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4), B(5,5)])
1203 # Try undoing these insertions
1205 expect_text("1\n\n2");
1206 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4)])
1209 expect_text("1\n2");
1210 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
1212 # Test deleting a range containing both results and statements
1216 insert(0, 0, "1\n2\n3\n4\n")
1218 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)])
1221 expect([S(0,0), R(1,1), S(2,2), R(3,3), B(4,4)])
1223 # Inserting an entire new statement in the middle
1224 insert(2, 0, "2.5\n")
1225 expect([S(0,0), R(1,1), S(2,2), S(3,3), R(4,4), B(5,5)])
1227 expect([S(0,0), R(1,1), S(2,2), R(3, 3), S(4, 4), R(5,5), B(6,6)])
1238 # Undoing insertion of a newline
1247 # Test the "pruning" behavior of modifications after undos
1254 buffer.redo() # does nothing
1258 # Test coalescing consecutive inserts
1266 # Test grouping of multiple undos by user actions
1270 buffer.begin_user_action()
1273 buffer.end_user_action()
1279 # Make sure that coalescing doesn't coalesce one user action with
1280 # only part of another
1284 buffer.begin_user_action()
1287 buffer.end_user_action()
1293 # Test an undo of an insert that caused insertion of result chunks
1297 expect([S(0,0), B(1,1)])
1299 expect([S(0,0), R(1,1), B(2,2)])
1303 expect([S(0,0), R(1,1), B(2,2)])
1307 # Try writing to a file, and reading it back
1314 SAVE_TEST
= """a = 1
1320 insert(0, 0, SAVE_TEST
)
1323 handle
, fname
= tempfile
.mkstemp(".txt", "shell_buffer")
1328 f
= open(fname
, "r")
1332 if saved
!= SAVE_TEST
:
1333 raise AssertionError("Got '%s', expected '%s'", saved
, SAVE_TEST
)
1338 expect([S(0,0), S(1,1), R(2,2), C(3,3), B(4,4), S(5,5)])