Another typo fix in the fontification code
[reinteract/rox.git] / lib / reinteract / shell_buffer.py
blob91a4d13777cb36dd7147adb54772a3cd8d97d613
1 #!/usr/bin/python
2 import gobject
3 import gtk
4 import traceback
5 import os
6 import re
7 from notebook import Notebook
8 from statement import Statement, ExecutionError
9 from worksheet import Worksheet
10 from custom_result import CustomResult
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" + str(e.cause)
101 self.error_line = e.traceback.tb_frame.f_lineno
102 self.error_offset = None
104 class BlankChunk:
105 def __init__(self, start=-1, end=-1, nr_start=-1):
106 self.start = start
107 self.end = end
108 self.nr_start = nr_start
110 def __repr__(self):
111 return "BlankChunk(%d,%d)" % (self.start, self.end)
113 class CommentChunk:
114 def __init__(self, start=-1, end=-1, nr_start=-1):
115 self.start = start
116 self.end = end
117 self.nr_start = nr_start
119 def __repr__(self):
120 return "CommentChunk(%d,%d)" % (self.start, self.end)
122 class ResultChunk:
123 def __init__(self, start=-1, end=-1, nr_start=-1):
124 self.start = start
125 self.end = end
126 self.nr_start = nr_start
128 def __repr__(self):
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:
136 pass
138 class ShellBuffer(gtk.TextBuffer, Worksheet):
139 __gsignals__ = {
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
165 # define it second
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"),
190 self.__lines = [""]
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)
198 self.filename = None
199 self.code_modified = False
201 def __compute_nr_start(self, chunk):
202 if chunk.start == 0:
203 chunk.nr_start = 0
204 else:
205 chunk_before = self.__chunks[chunk.start - 1]
206 if isinstance(chunk_before, ResultChunk):
207 chunk.nr_start = chunk_before.nr_start
208 else:
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):
212 changed_chunks = []
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])
218 old_statement = None
219 for i in xrange(chunk_start, statement_end + 1):
220 if isinstance(self.__chunks[i], StatementChunk):
221 old_statement = self.__chunks[i]
222 break
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)
238 else:
239 chunk = StatementChunk()
240 changed_lines = chunk.set_lines(chunk_lines)
241 changed = True
243 if changed:
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]
258 if i > 0:
259 chunk = self.__chunks[i - 1]
260 else:
261 chunk = None
263 if line == None:
264 # a ResultChunk Must be in the before-start portion, nothing needs doing
265 pass
266 elif BLANK.match(line):
267 if not isinstance(chunk, BlankChunk):
268 chunk = BlankChunk()
269 chunk.start = i
270 self.__compute_nr_start(chunk)
271 chunk.end = i
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()
277 chunk.start = i
278 self.__compute_nr_start(chunk)
279 chunk.end = i
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)
293 if result:
294 self.__apply_tag_to_chunk(self.__recompute_tag, result)
296 self.emit("chunk-status-changed", chunk)
297 if result:
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]
305 else:
306 old_text = self.__lines[rescan_start]
307 i = self.get_iter_at_line(rescan_start)
308 i_end = i.copy()
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):
315 rescan_start -= 1
316 else:
317 break
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:
325 rescan_end += 1
326 else:
327 break
329 chunk_start = rescan_start
330 statement_end = rescan_start - 1
331 chunk_lines = []
333 line = rescan_start
334 i = self.get_iter_at_line(rescan_start)
336 changed_chunks = []
338 for line in xrange(rescan_start, rescan_end + 1):
339 if line < start_line:
340 line_text = self.__lines[line]
341 else:
342 i_end = i.copy()
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)
355 statement_end = line
356 else:
357 changed_chunks.extend(self.__assign_lines(chunk_start, chunk_lines, statement_end))
358 chunk_start = line
359 statement_end = line
360 chunk_lines = [line_text]
362 i.forward_line()
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:
384 return
386 chunk = self.__chunks[start_line]
387 while chunk == None and start_line < end_line:
388 start_line += 1
389 chunk = self.__chunks[start_line]
391 if chunk == None:
392 return
394 last_chunk = self.__chunks[end_line]
395 while last_chunk == None:
396 end_line -= 1
397 last_chunk = self.__chunks[end_line]
399 while True:
400 yield chunk
401 if chunk == last_chunk:
402 break
403 line = chunk.end + 1
404 chunk = self.__chunks[line]
405 while chunk == None:
406 line += 1
407 chunk = self.__chunks[line]
409 def iterate_text(self):
410 iter = self.get_start_iter()
411 line = 0
412 chunk = self.__chunks[0]
414 while chunk != None:
415 next_line = chunk.end + 1
416 if next_line < len(self.__chunks):
417 next_chunk = self.__chunks[chunk.end + 1]
419 next = iter.copy()
420 while next.get_line() <= chunk.end:
421 next.forward_line()
422 else:
423 next_chunk = None
424 next = iter.copy()
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):
430 next.backward_line()
431 if not next.ends_line():
432 next.forward_to_line_end()
433 next_chunk = None
435 if not isinstance(chunk, ResultChunk):
436 chunk_text = self.get_slice(iter, next)
437 yield chunk_text
439 iter = next
440 line = next_line
441 chunk = next_chunk
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)
458 else:
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):
470 if len(nr_pos) == 2:
471 nr_line, offset = nr_pos
472 in_result = False
473 else:
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()
489 return iter
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):
506 chunk.start += 1
507 chunk.end += 1
508 chunk.nr_start += 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()):
522 return
523 # FIXME: PS
524 if not (text.startswith("\r") or text.startswith("\n")):
525 return
527 start_line += 1
528 is_pure_insert = True
530 if _verbose:
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:
541 return
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)
548 if is_pure_insert:
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)
558 else:
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])
576 if _verbose:
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)
584 i_end.forward_line()
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):
600 n_nr_deleted = 0
601 else:
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:
609 c.end -= n_deleted
610 elif c.end >= chunk.start:
611 c.end = chunk.start - 1
613 if c.start >= chunk.end:
614 c.start -= n_deleted
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):
622 return chunk
623 elif isinstance(chunk, StatementChunk):
624 return None
626 def __find_statement_for_result(self, result_chunk):
627 line = result_chunk.start - 1
628 while line >= 0:
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]
644 break
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
656 break
657 elif isinstance(self.__chunks[i], StatementChunk) and self.__chunks[i].start == i:
658 break
660 return state
662 def __fixup_results(self, state, revalidate_iters):
663 move_before = False
664 delete_after = False
665 move_after = False
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:
671 move_before = True
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:
677 delete_after = True
678 else:
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):
683 move_after = True
685 if not (move_before or delete_after or move_after):
686 return
688 if _verbose:
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)
693 if move_before:
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)
699 if move_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+)
719 if _verbose:
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
759 new_end = end.copy()
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:
766 return
768 end.backward_line()
769 if not new_end.ends_line():
770 new_end.forward_to_line_end()
771 end_line -= 1
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
782 else:
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
786 else:
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
791 if _verbose:
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:
802 return
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
812 n_nr_deleted = 0
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)
858 if _verbose:
859 print "After delete, chunks are", self.__chunks
861 def calculate(self):
862 parent = None
863 have_error = False
864 for chunk in self.iterate_chunks():
865 if isinstance(chunk, StatementChunk):
866 changed = False
867 if chunk.needs_compile or (chunk.needs_execute and not have_error):
868 old_result = self.__find_result(chunk)
869 if old_result:
870 self.__delete_chunk(old_result)
872 if chunk.needs_compile:
873 changed = True
874 chunk.compile(self)
875 if chunk.error_message != None:
876 self.insert_result(chunk)
878 if chunk.needs_execute and not have_error:
879 changed = True
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:
887 have_error = True
889 if changed:
890 self.emit("chunk-status-changed", chunk)
892 parent = chunk.statement
894 if _verbose:
895 print "After calculate, chunks are", self.__chunks
897 def get_chunk(self, line_index):
898 return self.__chunks[line_index]
900 def undo(self):
901 self.__undo_stack.undo()
903 def redo(self):
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()
911 return start, end
913 def __fontify_statement_lines(self, chunk, changed_lines):
914 iter = self.get_iter_at_line(chunk.start)
915 i = 0
916 for l in changed_lines:
917 while i < l:
918 iter.forward_line()
919 i += 1
920 end = iter.copy()
921 end.forward_line()
922 self.remove_all_tags(iter, end)
924 end = iter.copy()
925 for token_type, start_index, end_index in chunk.tokenized.get_tokens(l):
926 tag = self.__fontify_tags[token_type]
927 if tag != None:
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 ]
948 else:
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
976 return result_chunk
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):
983 return
985 self.filename = filename
986 self.code_modified = modified
988 if filename_changed:
989 self.emit('filename-changed')
991 if modified_changed:
992 self.emit('code-modified-changed')
994 def __set_modified(self, modified):
995 if modified == self.code_modified:
996 return
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())
1009 def clear(self):
1010 self.__do_clear()
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):
1017 f = open(filename)
1018 text = f.read()
1019 f.close()
1021 self.__do_clear()
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")
1040 success = False
1041 try:
1042 for chunk_text in self.iterate_text():
1043 f.write(chunk_text)
1045 f.close()
1046 os.rename(tmpname, filename)
1047 success = True
1049 self.__set_filename_and_modified(filename, False)
1050 finally:
1051 if not success:
1052 f.close()
1053 os.remove(tmpname)
1055 if __name__ == '__main__':
1056 S = StatementChunk
1057 B = BlankChunk
1058 C = CommentChunk
1059 R = ResultChunk
1061 def compare(l1, l2):
1062 if len(l1) != len(l2):
1063 return False
1065 for i in xrange(0, len(l1)):
1066 e1 = l1[i]
1067 e2 = l2[i]
1069 if type(e1) != type(e2) or e1.start != e2.start or e1.end != e2.end:
1070 return False
1072 return True
1074 buffer = ShellBuffer(Notebook())
1076 def validate_nr_start():
1077 n_nr = 0
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))
1089 validate_nr_start()
1091 def expect_text(expected):
1092 text = ""
1093 for chunk_text in buffer.iterate_text():
1094 text += chunk_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)
1111 def clear():
1112 buffer.clear()
1114 # Basic operation
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)])
1118 clear()
1119 expect([B(0,0)])
1121 # Turning a statement into a continuation line
1122 insert(0, 0, "1 \\\n+ 2\n")
1123 insert(1, 0, " ")
1124 expect([S(0,1), B(2,2)])
1126 # Calculation resulting in result chunks
1127 insert(2, 0, "3\n")
1128 buffer.calculate()
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
1133 delete(1, 0, 1, 1)
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
1137 clear()
1139 insert(0, 0, "1\\\n + 2\\\n + 3")
1140 delete(1, 0, 1, 1)
1141 expect([S(0,2)])
1143 # Editing a line with an existing error chunk to fix the error
1144 clear()
1146 insert(0, 0, "a\na=2")
1147 buffer.calculate()
1149 insert(0, 0, "2")
1150 delete(0, 1, 0, 2)
1151 buffer.calculate()
1152 expect([S(0,0), R(1,1), S(2,2)])
1154 # Deleting an entire continuation line
1155 clear()
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)])
1159 delete(1, 0, 2, 0)
1160 expect([S(0,1), B(2,2)])
1162 # Test an attempt to join a ResultChunk onto a previous chunk; should ignore
1163 clear()
1165 insert(0, 0, "1\n");
1166 buffer.calculate()
1167 expect([S(0,0), R(1,1), B(2,2)])
1168 delete(0, 1, 1, 0)
1169 expect_text("1\n");
1171 # Test an attempt to join a chunk onto a previous ResultChunk, should move
1172 # the ResultChunk and do the modification
1173 clear()
1175 insert(0, 0, "1\n2\n");
1176 buffer.calculate()
1177 expect([S(0,0), R(1,1), S(2,2), R(3,3), B(4,4)])
1178 delete(1, 1, 2, 0)
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
1183 clear()
1185 insert(0, 0, "1\n2");
1186 buffer.calculate()
1187 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
1188 insert(1, 0, "foo")
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
1193 # a new line
1194 insert(1, 1, "\n")
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
1199 insert(4, 1, "\n")
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
1204 buffer.undo()
1205 expect_text("1\n\n2");
1206 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4)])
1208 buffer.undo()
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
1214 clear()
1216 insert(0, 0, "1\n2\n3\n4\n")
1217 buffer.calculate()
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)])
1220 delete(2, 0, 5, 0)
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)])
1226 buffer.calculate()
1227 expect([S(0,0), R(1,1), S(2,2), R(3, 3), S(4, 4), R(5,5), B(6,6)])
1229 # Undo tests
1230 clear()
1232 insert(0, 0, "1")
1233 buffer.undo()
1234 expect_text("")
1235 buffer.redo()
1236 expect_text("1")
1238 # Undoing insertion of a newline
1239 clear()
1241 insert(0, 0, "1 ")
1242 insert(0, 1, "\n")
1243 buffer.calculate()
1244 buffer.undo()
1245 expect_text("1 ")
1247 # Test the "pruning" behavior of modifications after undos
1248 clear()
1250 insert(0, 0, "1")
1251 buffer.undo()
1252 expect_text("")
1253 insert(0, 0, "2")
1254 buffer.redo() # does nothing
1255 expect_text("2")
1256 insert(0, 0, "2\n")
1258 # Test coalescing consecutive inserts
1259 clear()
1261 insert(0, 0, "1")
1262 insert(0, 1, "2")
1263 buffer.undo()
1264 expect_text("")
1266 # Test grouping of multiple undos by user actions
1267 clear()
1269 insert(0, 0, "1")
1270 buffer.begin_user_action()
1271 delete(0, 0, 0, 1)
1272 insert(0, 0, "2")
1273 buffer.end_user_action()
1274 buffer.undo()
1275 expect_text("1")
1276 buffer.redo()
1277 expect_text("2")
1279 # Make sure that coalescing doesn't coalesce one user action with
1280 # only part of another
1281 clear()
1283 insert(0, 0, "1")
1284 buffer.begin_user_action()
1285 insert(0, 1, "2")
1286 delete(0, 0, 0, 1)
1287 buffer.end_user_action()
1288 buffer.undo()
1289 expect_text("1")
1290 buffer.redo()
1291 expect_text("2")
1293 # Test an undo of an insert that caused insertion of result chunks
1294 clear()
1296 insert(0, 0, "2\n")
1297 expect([S(0,0), B(1,1)])
1298 buffer.calculate()
1299 expect([S(0,0), R(1,1), B(2,2)])
1300 insert(0, 0, "1\n")
1301 buffer.calculate()
1302 buffer.undo()
1303 expect([S(0,0), R(1,1), B(2,2)])
1304 expect_text("2\n")
1307 # Try writing to a file, and reading it back
1309 import tempfile, os
1311 clear()
1312 expect([B(0,0)])
1314 SAVE_TEST = """a = 1
1316 # A comment
1318 b = 2"""
1320 insert(0, 0, SAVE_TEST)
1321 buffer.calculate()
1323 handle, fname = tempfile.mkstemp(".txt", "shell_buffer")
1324 os.close(handle)
1326 try:
1327 buffer.save(fname)
1328 f = open(fname, "r")
1329 saved = f.read()
1330 f.close()
1332 if saved != SAVE_TEST:
1333 raise AssertionError("Got '%s', expected '%s'", saved, SAVE_TEST)
1335 buffer.load(fname)
1336 buffer.calculate()
1338 expect([S(0,0), S(1,1), R(2,2), C(3,3), B(4,4), S(5,5)])
1339 finally:
1340 os.remove(fname)
1342 clear()
1343 expect([B(0,0)])