Merge commit 'origin/master'
[reinteract/rox.git] / lib / reinteract / shell_buffer.py
blob6e891f56c930f068e2551ca81b9c8775b66b98f4
1 #!/usr/bin/python
2 import gobject
3 import gtk
4 import pango
5 import logging
6 import traceback
7 import os
8 import re
9 import sys
10 import doc_format
11 from notebook import Notebook, HelpResult
12 from statement import Statement, ExecutionError, WarningResult
13 from worksheet import Worksheet
14 from custom_result import CustomResult
15 import tokenize
16 from tokenized_statement import TokenizedStatement
17 from undo_stack import UndoStack, InsertOp, DeleteOp
19 # See comment in iter_copy_from.py
20 try:
21 gtk.TextIter.copy_from
22 def _copy_iter(dest, src):
23 dest.copy_from(src)
24 except AttributeError:
25 from iter_copy_from import iter_copy_from as _copy_iter
27 _debug = logging.getLogger("ShellBuffer").debug
29 class StatementChunk:
30 def __init__(self, start=-1, end=-1, nr_start=-1):
31 self.start = start
32 self.end = end
33 # this is the start index ignoring result chunks; we need this for
34 # storing items in the undo stack
35 self.nr_start = nr_start
36 # This is a count maintained by the buffer as to how many lines reference
37 # the statement; it's used to determine when we are deleting a chunk
38 # from the buffer
39 self.line_count = 0
40 self.tokenized = TokenizedStatement()
42 self.needs_compile = False
43 self.needs_execute = False
44 self.statement = None
46 self.results = None
48 self.error_message = None
49 self.error_line = None
50 self.error_offset = None
52 def __repr__(self):
53 return "StatementChunk(%d,%d,%s,%s,'%s')" % (self.start, self.end, self.needs_compile, self.needs_execute, self.tokenized.get_text())
55 def set_lines(self, lines):
56 changed_lines = self.tokenized.set_lines(lines)
57 if changed_lines == None:
58 return None
60 self.needs_compile = True
61 self.needs_execute = False
63 self.statement = None
65 return changed_lines
67 def mark_for_execute(self):
68 if self.statement == None or self.needs_execute:
69 return False
70 else:
71 self.needs_execute = True
72 return True
74 def compile(self, worksheet):
75 if self.statement != None:
76 return
78 self.needs_compile = False
80 self.results = None
82 self.error_message = None
83 self.error_line = None
84 self.error_offset = None
86 try:
87 self.statement = Statement(self.tokenized.get_text(), worksheet)
88 self.needs_execute = True
89 except SyntaxError, e:
90 self.error_message = e.msg
91 self.error_line = e.lineno
92 self.error_offset = e.offset
94 def execute(self, parent):
95 assert(self.statement != None)
97 self.needs_compile = False
98 self.needs_execute = False
100 self.error_message = None
101 self.error_line = None
102 self.error_offset = None
104 try:
105 self.statement.set_parent(parent)
106 self.statement.execute()
107 self.results = self.statement.results
108 except ExecutionError, e:
109 self.error_message = "\n".join(traceback.format_tb(e.traceback)[2:]) + "\n".join(traceback.format_exception_only(e.type, e.value))
110 if self.error_message.endswith("\n"):
111 self.error_message = self.error_message[0:-1]
113 self.error_line = e.traceback.tb_frame.f_lineno
114 self.error_offset = None
116 class BlankChunk:
117 def __init__(self, start=-1, end=-1, nr_start=-1):
118 self.start = start
119 self.end = end
120 self.nr_start = nr_start
121 self.line_count = 0
123 def __repr__(self):
124 return "BlankChunk(%d,%d)" % (self.start, self.end)
126 class CommentChunk:
127 def __init__(self, start=-1, end=-1, nr_start=-1):
128 self.start = start
129 self.end = end
130 self.nr_start = nr_start
131 self.line_count = 0
133 def __repr__(self):
134 return "CommentChunk(%d,%d)" % (self.start, self.end)
136 class ResultChunk:
137 def __init__(self, start=-1, end=-1, nr_start=-1):
138 self.start = start
139 self.end = end
140 self.nr_start = nr_start
141 self.line_count = 0
143 def __repr__(self):
144 return "ResultChunk(%d,%d)" % (self.start, self.end)
146 BLANK = re.compile(r'^\s*$')
147 COMMENT = re.compile(r'^\s*#')
148 CONTINUATION = re.compile(r'^(?:\s+|(?:except|finally)[^A-Za-z0-9_])')
150 class ResultChunkFixupState:
151 pass
153 class ShellBuffer(gtk.TextBuffer, Worksheet):
154 __gsignals__ = {
155 'begin-user-action': 'override',
156 'end-user-action': 'override',
157 'insert-text': 'override',
158 'delete-range': 'override',
159 'chunk-status-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
160 'add-custom-result': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
161 'pair-location-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
163 # It would be more GObject to make these properties, but we'll wait on that until
164 # decent property support lands:
166 # http://blogs.gnome.org/johan/2007/04/30/simplified-gobject-properties/
168 'filename-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
169 # Clumsy naming is because GtkTextBuffer already has a modified flag, but that would
170 # include changes to the results
171 'code-modified-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
174 def __init__(self, notebook):
175 gtk.TextBuffer.__init__(self)
176 Worksheet.__init__(self, notebook)
178 self.__red_tag = self.create_tag(foreground="red")
179 self.__result_tag = self.create_tag(family="monospace", style="italic", wrap_mode=gtk.WRAP_WORD, editable=False)
180 # Order here is significant ... we want the recompute tag to have higher priority, so
181 # define it second
182 self.__warning_tag = self.create_tag(foreground="#aa8800")
183 self.__error_tag = self.create_tag(foreground="#aa0000")
184 self.__recompute_tag = self.create_tag(foreground="#888888")
185 self.__comment_tag = self.create_tag(foreground="#3f7f5f")
186 self.__bold_tag = self.create_tag(weight=pango.WEIGHT_BOLD)
187 self.__help_tag = self.create_tag(family="sans",
188 style=pango.STYLE_NORMAL,
189 paragraph_background="#ffff88",
190 left_margin=10,
191 right_margin=10)
193 punctuation_tag = None
195 self.__fontify_tags = {
196 tokenize.TOKEN_KEYWORD : self.create_tag(foreground="#7f0055", weight=600),
197 tokenize.TOKEN_NAME : None,
198 tokenize.TOKEN_COMMENT : self.__comment_tag,
199 tokenize.TOKEN_BUILTIN_CONSTANT : self.create_tag(foreground="#55007f"),
200 tokenize.TOKEN_STRING : self.create_tag(foreground="#00aa00"),
201 tokenize.TOKEN_PUNCTUATION : punctuation_tag,
202 tokenize.TOKEN_CONTINUATION : punctuation_tag,
203 tokenize.TOKEN_LPAREN : punctuation_tag,
204 tokenize.TOKEN_RPAREN : punctuation_tag,
205 tokenize.TOKEN_LSQB : punctuation_tag,
206 tokenize.TOKEN_RSQB : punctuation_tag,
207 tokenize.TOKEN_LBRACE : punctuation_tag,
208 tokenize.TOKEN_RBRACE : punctuation_tag,
209 tokenize.TOKEN_BACKQUOTE : punctuation_tag,
210 tokenize.TOKEN_COLON : punctuation_tag,
211 tokenize.TOKEN_DOT : punctuation_tag,
212 tokenize.TOKEN_EQUAL : punctuation_tag,
213 tokenize.TOKEN_AUGEQUAL : punctuation_tag,
214 tokenize.TOKEN_NUMBER : None,
215 tokenize.TOKEN_JUNK : self.create_tag(underline="error"),
218 self.__lines = [""]
219 self.__chunks = [BlankChunk(0,0, 0)]
220 self.__modifying_results = False
221 self.__applying_undo = False
222 self.__user_action_count = 0
224 self.__changed_chunks = {}
225 self.__freeze_changes_count = 0
227 self.__have_pair = False
228 self.__pair_mark = self.create_mark(None, self.get_start_iter(), True)
230 self.__undo_stack = UndoStack(self)
232 self.filename = None
233 self.code_modified = False
235 def __freeze_changes(self):
236 self.__freeze_changes_count += 1
238 def __thaw_changes(self):
239 self.__freeze_changes_count -= 1
240 if self.__freeze_changes_count == 0 and len(self.__changed_chunks) > 0:
241 changed = self.__changed_chunks
242 self.__changed_chunks = {}
243 for chunk in sorted(changed.keys(), lambda a, b: cmp(a.start, b.start)):
244 changed_lines = changed[chunk]
245 if (changed_lines != None and len(changed_lines) > 0):
246 self.__fontify_statement_lines(chunk, changed_lines)
248 result = self.__find_result(chunk)
249 if result:
250 self.__apply_tag_to_chunk(self.__recompute_tag, result)
252 self.emit("chunk-status-changed", chunk)
253 if result:
254 self.emit("chunk-status-changed", result)
256 def __chunk_changed(self, chunk, changed_lines):
257 if self.__freeze_changes_count == 0:
258 # We could add a free/thaw pair, but it's better to catch our mistakes
259 # and add the freeze/thaw pair at the outer level
260 raise RuntimeException("Chunks changed without a __freeze_changes() pair")
261 else:
262 if chunk in self.__changed_chunks:
263 if changed_lines != None:
264 old = self.__changed_chunks[chunk]
265 if old == None:
266 self.__changed_chunks[chunk] = changed_lines
267 else:
268 s = set(changed_lines)
269 s.update(old)
270 self.__changed_chunks[chunk] = sorted(s)
271 else:
272 self.__changed_chunks[chunk] = changed_lines
274 def __mark_rest_for_execute(self, start_line):
275 # Mark all statements starting from start_line as needing execution.
276 # We do this immediately when we change or delete a previous
277 # StatementChunk. The alternative would be to do it when we
278 # __thaw_changes(), which would conceivably be more efficient, but
279 # it's hard to see how to handle deleted chunks in that case.
281 for chunk in self.iterate_chunks(start_line):
282 if isinstance(chunk, StatementChunk):
283 if chunk.mark_for_execute():
284 self.__chunk_changed(chunk, None)
285 else:
286 # Everything after the first chunk that was previously
287 # marked for execution must also have been marked for
288 # execution, so we can stop
289 break
291 def __mark_changed_statement(self, chunk, changed_lines):
292 self.__chunk_changed(chunk, changed_lines)
293 self.__mark_rest_for_execute(chunk.end + 1)
295 def __decrement_line_count(self, chunk, line):
296 if chunk != None:
297 chunk.line_count -= 1
298 if chunk.line_count == 0:
299 try:
300 del self.__changed_chunks[chunk]
301 except KeyError:
302 pass
303 if isinstance(chunk, StatementChunk):
304 self.__mark_rest_for_execute(line + 1)
306 def __set_line(self, i, chunk, text):
307 old_chunk = self.__chunks[i]
308 self.__chunks[i] = chunk
309 self.__lines[i] = text
310 chunk.line_count += 1
311 self.__decrement_line_count(old_chunk, i)
313 def __insert_line(self, start, chunk, text):
314 self.__chunks[start:start] = [chunk]
315 self.__lines[count:start] = [text]
316 chunk.line_count += 1
318 def __insert_lines(self, start, count, chunk=None):
319 self.__chunks[start:start] = (chunk for i in xrange(count))
320 self.__lines[start:start] = (None for i in xrange(count))
321 if chunk != None:
322 chunk.line_count += count
324 def __clear_line_chunks(self, start, end):
325 for i in xrange(start, end + 1):
326 self.__decrement_line_count(self.__chunks[i], i)
327 self.__chunks[i] = None
329 def __delete_lines(self, start, end):
330 for i in xrange(start, end+1):
331 self.__decrement_line_count(self.__chunks[i], i)
333 del self.__chunks[start:end + 1]
334 del self.__lines[start:end + 1]
336 def __compute_nr_start(self, chunk):
337 if chunk.start == 0:
338 chunk.nr_start = 0
339 else:
340 chunk_before = self.__chunks[chunk.start - 1]
341 if isinstance(chunk_before, ResultChunk):
342 chunk.nr_start = chunk_before.nr_start
343 else:
344 chunk.nr_start = chunk_before.nr_start + (1 + chunk_before.end - chunk_before.start)
346 def __assign_lines(self, chunk_start, lines, statement_end):
347 if statement_end >= chunk_start:
348 def notnull(l): return l != None
349 chunk_lines = filter(notnull, lines[0:statement_end + 1 - chunk_start])
351 old_statement = None
352 for i in xrange(chunk_start, statement_end + 1):
353 if isinstance(self.__chunks[i], StatementChunk):
354 old_statement = self.__chunks[i]
355 break
357 if old_statement != None:
358 # An old statement can only be turned into *one* new statement; this
359 # prevents us getting fooled if we split a statement
360 self.__clear_line_chunks(max(old_statement.start, statement_end + 1), old_statement.end)
362 chunk = old_statement
363 old_needs_compile = chunk.needs_compile
364 changed_lines = chunk.set_lines(chunk_lines)
365 changed = chunk.needs_compile != old_needs_compile
367 # If we moved the statement with respect to the buffer, then the we
368 # need to refontify, even if the old statement didn't change
369 if old_statement.start != chunk_start:
370 changed_lines = range(0, 1 + statement_end - chunk_start)
371 else:
372 chunk = StatementChunk()
373 changed_lines = chunk.set_lines(chunk_lines)
374 changed = True
376 chunk.start = chunk_start
377 chunk.end = statement_end
378 self.__compute_nr_start(chunk)
380 for i in xrange(chunk_start, statement_end + 1):
381 self.__set_line(i, chunk, lines[i - chunk_start])
383 if changed or changed_lines != None and len(changed_lines) > 0:
384 self.__mark_changed_statement(chunk, changed_lines)
386 for i in xrange(statement_end + 1, chunk_start + len(lines)):
387 line = lines[i - chunk_start]
389 if i > 0:
390 chunk = self.__chunks[i - 1]
391 else:
392 chunk = None
394 if line == None:
395 # a ResultChunk Must be in the before-start portion, nothing needs doing
396 pass
397 elif BLANK.match(line):
398 if not isinstance(chunk, BlankChunk):
399 chunk = BlankChunk()
400 chunk.start = i
401 self.__compute_nr_start(chunk)
402 chunk.end = i
403 self.__set_line(i, chunk, lines[i - chunk_start])
404 elif COMMENT.match(line):
405 if not isinstance(chunk, CommentChunk):
406 chunk = CommentChunk()
407 chunk.start = i
408 self.__compute_nr_start(chunk)
409 chunk.end = i
410 self.__set_line(i, chunk, lines[i - chunk_start])
411 # This is O(n^2) inefficient
412 self.__apply_tag_to_chunk(self.__comment_tag, chunk, remove_old=True)
414 def __rescan(self, start_line, end_line):
415 rescan_start = start_line
416 while rescan_start > 0:
417 if rescan_start < start_line:
418 new_text = old_text = self.__lines[rescan_start]
419 else:
420 old_text = self.__lines[rescan_start]
421 i = self.get_iter_at_line(rescan_start)
422 i_end = i.copy()
423 if not i_end.ends_line():
424 i_end.forward_to_line_end()
425 new_text = self.get_slice(i, i_end)
427 if old_text == None or BLANK.match(old_text) or COMMENT.match(old_text) or CONTINUATION.match(old_text) or \
428 new_text == None or BLANK.match(new_text) or COMMENT.match(new_text) or CONTINUATION.match(new_text):
429 rescan_start -= 1
430 else:
431 break
433 # If previous contents of the modified range ended within a statement, then we need to rescan all of it;
434 # since we may have already deleted all of the statement lines within the modified range, we detect
435 # this case by seeing if the line *after* our range is a continuation line.
436 rescan_end = end_line
437 while rescan_end + 1 < len(self.__chunks):
438 if isinstance(self.__chunks[rescan_end + 1], StatementChunk) and self.__chunks[rescan_end + 1].start != rescan_end + 1:
439 rescan_end += 1
440 else:
441 break
443 chunk_start = rescan_start
444 statement_end = rescan_start - 1
445 chunk_lines = []
447 line = rescan_start
448 i = self.get_iter_at_line(rescan_start)
450 for line in xrange(rescan_start, rescan_end + 1):
451 if line < start_line:
452 line_text = self.__lines[line]
453 else:
454 i_end = i.copy()
455 if not i_end.ends_line():
456 i_end.forward_to_line_end()
457 line_text = self.get_slice(i, i_end)
459 if line_text == None:
460 chunk_lines.append(line_text)
461 elif BLANK.match(line_text):
462 chunk_lines.append(line_text)
463 elif COMMENT.match(line_text):
464 chunk_lines.append(line_text)
465 elif CONTINUATION.match(line_text):
466 chunk_lines.append(line_text)
467 statement_end = line
468 else:
469 self.__assign_lines(chunk_start, chunk_lines, statement_end)
470 chunk_start = line
471 statement_end = line
472 chunk_lines = [line_text]
474 i.forward_line()
476 self.__assign_lines(chunk_start, chunk_lines, statement_end)
478 def iterate_chunks(self, start_line=0, end_line=None):
479 if end_line == None or end_line >= len(self.__chunks):
480 end_line = len(self.__chunks) - 1
481 if start_line >= len(self.__chunks) or end_line < start_line:
482 return
484 chunk = self.__chunks[start_line]
485 while chunk == None and start_line < end_line:
486 start_line += 1
487 chunk = self.__chunks[start_line]
489 if chunk == None:
490 return
492 last_chunk = self.__chunks[end_line]
493 while last_chunk == None:
494 end_line -= 1
495 last_chunk = self.__chunks[end_line]
497 while True:
498 yield chunk
499 if chunk == last_chunk:
500 break
501 try:
502 line = chunk.end + 1
503 chunk = self.__chunks[line]
504 while chunk == None:
505 line += 1
506 chunk = self.__chunks[line]
507 except IndexError:
508 # This happens if the last chunk was removed; just
509 # proceeding to the end of the buffer isn't always
510 # going to be right, but it is right in the case
511 # where we are iterating the whole buffer, which
512 # is what happens for calculate()
513 return
515 def iterate_text(self, start=None, end=None):
516 result = ""
518 if start == None:
519 start = self.get_start_iter()
520 if end == None:
521 end = self.get_end_iter()
523 start_chunk = self.__chunks[start.get_line()]
524 end_chunk = self.__chunks[end.get_line()]
526 # special case .. if start/end are in the same chunk, get the text
527 # between them, even if the chunk is a ResultChunk.
528 if start_chunk == end_chunk:
529 yield self.get_slice(start, end)
530 return
532 chunk = start_chunk
533 iter = self.get_iter_at_line(chunk.start)
535 while True:
536 next_line = chunk.end + 1
537 if next_line < len(self.__chunks):
538 next_chunk = self.__chunks[chunk.end + 1]
540 next = iter.copy()
541 while next.get_line() <= chunk.end:
542 next.forward_line()
543 else:
544 next_chunk = None
545 next = iter.copy()
546 next.forward_to_end()
548 # Special case .... if the last chunk is a ResultChunk, then we don't
549 # want to include the new line from the previous line
550 if isinstance(next_chunk, ResultChunk) and next_chunk.end + 1 == len(self.__chunks):
551 next.backward_line()
552 if not next.ends_line():
553 next.forward_to_line_end()
554 next_chunk = None
556 if not isinstance(chunk, ResultChunk):
557 chunk_start, chunk_end = iter, next
558 if chunk == start_chunk:
559 chunk_start = start
560 else:
561 chunk_start = iter
563 if chunk == end_chunk:
564 chunk_end = end
565 else:
566 chunk_end = next
568 yield self.get_slice(chunk_start, chunk_end)
570 iter = next
571 line = next_line
572 if chunk == end_chunk or next_chunk == None:
573 break
574 else:
575 chunk = next_chunk
577 def get_public_text(self, start=None, end=None):
578 return "".join(self.iterate_text(start, end))
580 def do_begin_user_action(self):
581 self.__user_action_count += 1
582 self.__undo_stack.begin_user_action()
583 self.__freeze_changes()
585 def do_end_user_action(self):
586 self.__user_action_count -= 1
587 self.__thaw_changes()
588 self.__undo_stack.end_user_action()
590 def __compute_nr_pos_from_chunk_offset(self, chunk, line, offset):
591 if isinstance(chunk, ResultChunk):
592 prev_chunk = self.__chunks[chunk.start - 1]
593 iter = self.get_iter_at_line(prev_chunk.end)
594 if not iter.ends_line():
595 iter.forward_to_line_end()
596 return (prev_chunk.end - prev_chunk.start + prev_chunk.nr_start, iter.get_line_offset(), 1)
597 else:
598 return (line - chunk.start + chunk.nr_start, offset)
600 def __compute_nr_pos_from_iter(self, iter):
601 line = iter.get_line()
602 chunk = self.__chunks[line]
603 return self.__compute_nr_pos_from_chunk_offset(chunk, line, iter.get_line_offset())
605 def __compute_nr_pos_from_line_offset(self, line, offset):
606 return self.__compute_nr_pos_from_chunk_offset(self.__chunks[line], line, offset)
608 def _get_iter_at_nr_pos(self, nr_pos):
609 if len(nr_pos) == 2:
610 nr_line, offset = nr_pos
611 in_result = False
612 else:
613 nr_line, offset, in_result = nr_pos
615 for chunk in self.iterate_chunks():
616 if not isinstance(chunk, ResultChunk) and chunk.nr_start + (chunk.end - chunk.start) >= nr_line:
617 line = chunk.start + nr_line - chunk.nr_start
618 iter = self.get_iter_at_line(line)
619 iter.set_line_offset(offset)
621 if in_result and chunk.end + 1 < len(self.__chunks):
622 next_chunk = self.__chunks[chunk.end + 1]
623 if isinstance(next_chunk, ResultChunk):
624 iter = self.get_iter_at_line(next_chunk.end)
625 if not iter.ends_line():
626 iter.forward_to_line_end()
628 return iter
630 raise AssertionError("nr_pos pointed outside buffer")
633 def __insert_blank_line_after(self, chunk_before, location, separator):
634 start_pos = self.__compute_nr_pos_from_iter(location)
636 self.__modifying_results = True
637 gtk.TextBuffer.do_insert_text(self, location, separator, len(separator))
638 self.__modifying_results = False
640 new_chunk = BlankChunk(chunk_before.end + 1, chunk_before.end + 1, chunk_before.nr_start)
641 self.__insert_line(chunk_before.end + 1, new_chunk, "")
643 for chunk in self.iterate_chunks(new_chunk.end + 1):
644 chunk.start += 1
645 chunk.end += 1
646 chunk.nr_start += 1
648 end_pos = self.__compute_nr_pos_from_iter(location)
649 self.__undo_stack.append_op(InsertOp(start_pos, end_pos, separator))
651 def do_insert_text(self, location, text, text_len):
652 start_line = location.get_line()
653 start_offset = location.get_line_offset()
654 is_pure_insert = False
655 if self.__user_action_count > 0 and not self.__modifying_results:
656 current_chunk = self.__chunks[start_line]
657 if isinstance(current_chunk, ResultChunk):
658 # The only thing that's valid to do with a ResultChunk is insert
659 # a newline at the end to get another line after it
660 if not (start_line == current_chunk.end and location.ends_line()):
661 return
662 # FIXME: PS
663 if not (text.startswith("\r") or text.startswith("\n")):
664 return
666 start_line += 1
667 is_pure_insert = True
669 if not self.__modifying_results:
670 _debug("Inserting '%s' at %s", text, (location.get_line(), location.get_line_offset()))
672 if not self.__modifying_results:
673 start_pos = self.__compute_nr_pos_from_iter(location)
675 gtk.TextBuffer.do_insert_text(self, location, text, text_len)
676 end_line = location.get_line()
677 end_offset = location.get_line_offset()
679 if self.__modifying_results:
680 return
682 self.__freeze_changes()
684 if self.__user_action_count > 0:
685 self.__set_modified(True)
687 result_fixup_state = self.__get_result_fixup_state(start_line, start_line)
689 if is_pure_insert:
690 self.__insert_lines(start_line, end_line + 1 - start_line)
692 for chunk in self.iterate_chunks(end_line + 1):
693 if chunk.start >= start_line:
694 chunk.start += (1 + end_line - start_line)
695 chunk.nr_start += (1 + end_line - start_line)
696 if chunk.end >= start_line:
697 chunk.end += (1 + end_line - start_line)
698 else:
699 # If we are inserting at the beginning of a line, then the insert moves the
700 # old chunk down, or leaves it in place, so insert new lines at the start position.
701 # If we insert elsewhere it either splits the chunk (and we consider
702 # that leaving the old chunk at the start) or inserts stuff after the chunk,
703 # so insert new lines after the start position.
704 if start_offset == 0:
705 self.__insert_lines(start_line, end_line - start_line)
707 for chunk in self.iterate_chunks(start_line):
708 if chunk.start >= start_line:
709 chunk.start += (end_line - start_line)
710 chunk.nr_start += (end_line - start_line)
711 if chunk.end >= start_line:
712 chunk.end += (end_line - start_line)
713 else:
714 self.__insert_lines(start_line + 1, end_line - start_line)
716 for chunk in self.iterate_chunks(start_line):
717 if chunk.start > start_line:
718 chunk.start += (end_line - start_line)
719 chunk.nr_start += (end_line - start_line)
720 if chunk.end > start_line:
721 chunk.end += (end_line - start_line)
723 self.__rescan(start_line, end_line)
725 end_pos = self.__compute_nr_pos_from_line_offset(end_line, end_offset)
726 self.__undo_stack.append_op(InsertOp(start_pos, end_pos, text[0:text_len]))
728 self.__fixup_results(result_fixup_state, [location])
729 self.__thaw_changes()
730 self.__calculate_pair_location()
732 _debug("After insert, chunks are %s", self.__chunks)
734 def __delete_chunk(self, chunk):
735 self.__modifying_results = True
736 self.__freeze_changes()
738 i_start = self.get_iter_at_line(chunk.start)
739 i_end = self.get_iter_at_line(chunk.end)
740 i_end.forward_line()
741 if i_end.get_line() == chunk.end:
742 # Last line of buffer, need to delete the chunk and not
743 # leave a trailing newline
744 if not i_end.ends_line():
745 i_end.forward_to_line_end()
746 i_start.backward_line()
747 if not i_start.ends_line():
748 i_start.forward_to_line_end()
749 self.delete(i_start, i_end)
751 self.__delete_lines(chunk.start, chunk.end)
753 n_deleted = chunk.end + 1 - chunk.start
754 if isinstance(chunk, ResultChunk):
755 n_nr_deleted = 0
756 else:
757 n_deleted = n_nr_deleted
759 # Overlapping chunks can occur temporarily when inserting
760 # or deleting text merges two adjacent statements with a ResultChunk in between, so iterate
761 # all chunks, not just the ones after the deleted chunk
762 for c in self.iterate_chunks():
763 if c.end >= chunk.end:
764 c.end -= n_deleted
765 elif c.end >= chunk.start:
766 c.end = chunk.start - 1
768 if c.start >= chunk.end:
769 c.start -= n_deleted
770 c.nr_start -= n_nr_deleted
772 self.__thaw_changes()
773 self.__modifying_results = False
775 def __find_result(self, statement):
776 for chunk in self.iterate_chunks(statement.end + 1):
777 if isinstance(chunk, ResultChunk):
778 return chunk
779 elif isinstance(chunk, StatementChunk):
780 return None
782 def __find_statement_for_result(self, result_chunk):
783 line = result_chunk.start - 1
784 while line >= 0:
785 if isinstance(self.__chunks[line], StatementChunk):
786 return self.__chunks[line]
787 raise AssertionError("Result with no corresponding statement")
789 def __get_result_fixup_state(self, first_modified_line, last_modified_line):
790 state = ResultChunkFixupState()
792 state.statement_before = None
793 state.result_before = None
794 for i in xrange(first_modified_line - 1, -1, -1):
795 if isinstance(self.__chunks[i], ResultChunk):
796 state.result_before = self.__chunks[i]
797 elif isinstance(self.__chunks[i], StatementChunk):
798 if state.result_before != None:
799 state.statement_before = self.__chunks[i]
800 break
802 state.statement_after = None
803 state.result_after = None
805 for i in xrange(last_modified_line + 1, len(self.__chunks)):
806 if isinstance(self.__chunks[i], ResultChunk):
807 state.result_after = self.__chunks[i]
808 for j in xrange(i - 1, -1, -1):
809 if isinstance(self.__chunks[j], StatementChunk):
810 state.statement_after = self.__chunks[j]
811 assert state.statement_after.results != None or state.statement_after.error_message != None
812 break
813 elif isinstance(self.__chunks[i], StatementChunk) and self.__chunks[i].start == i:
814 break
816 return state
818 def __fixup_results(self, state, revalidate_iters):
819 move_before = False
820 delete_after = False
821 move_after = False
823 if state.result_before != None:
824 # If lines were added into the StatementChunk that produced the ResultChunk above the edited segment,
825 # then the ResultChunk needs to be moved after the newly inserted lines
826 if state.statement_before.end > state.result_before.start:
827 move_before = True
829 if state.result_after != None:
830 # If the StatementChunk that produced the ResultChunk after the edited segment was deleted, then the
831 # ResultChunk needs to be deleted as well
832 if self.__chunks[state.statement_after.start] != state.statement_after:
833 delete_after = True
834 else:
835 # If another StatementChunk was inserted between the StatementChunk and the ResultChunk, then we
836 # need to move the ResultChunk above that statement
837 for i in xrange(state.statement_after.end + 1, state.result_after.start):
838 if self.__chunks[i] != state.statement_after and isinstance(self.__chunks[i], StatementChunk):
839 move_after = True
841 if not (move_before or delete_after or move_after):
842 return
844 _debug("Result fixups: move_before=%s, delete_after=%s, move_after=%s", move_before, delete_after, move_after)
846 revalidate = map(lambda iter: (iter, self.create_mark(None, iter, True)), revalidate_iters)
848 if move_before:
849 self.__delete_chunk(state.result_before)
850 self.insert_result(state.statement_before)
852 if delete_after or move_after:
853 self.__delete_chunk(state.result_after)
854 if move_after:
855 self.insert_result(state.statement_after)
857 for iter, mark in revalidate:
858 _copy_iter(iter, self.get_iter_at_mark(mark))
859 self.delete_mark(mark)
861 def do_delete_range(self, start, end):
863 # Note that there is a bug in GTK+ versions prior to 2.12.2, where it doesn't work
864 # if a ::delete-range handler deletes stuff outside it's requested range. (No crash,
865 # gtk_text_buffer_delete_interactive() just leaves some editable text undeleleted.)
866 # See: http://bugzilla.gnome.org/show_bug.cgi?id=491207
868 # The only workaround I can think of right now would be to stop using not-editable
869 # tags on results, and implement the editability ourselves in ::insert-text
870 # and ::delete-range, but a) that's a lot of work to rewrite that way b) it will make
871 # the text view give worse feedback. So, I'm just leaving the problem for now,
872 # (and have committed the fix to GTK+)
874 if not self.__modifying_results:
875 _debug("Request to delete range (%s,%s)-(%s,%s)", start.get_line(), start.get_line_offset(), end.get_line(), end.get_line_offset())
876 start_line = start.get_line()
877 end_line = end.get_line()
879 restore_result_statement = None
881 # Prevent the user from doing deletes that would merge a ResultChunk chunk with another chunk
882 if self.__user_action_count > 0 and not self.__modifying_results:
883 if start.ends_line() and isinstance(self.__chunks[start_line], ResultChunk):
884 # Merging another chunk onto the end of a ResultChunk; e.g., hitting delete at the
885 # start of a line with a ResultChunk before it. We don't want to actually ignore this,
886 # since otherwise if you split a line, you can't join it back up, instead we actually
887 # have to do what the user wanted to do ... join the two lines.
889 # We delete the result chunk, and if everything still looks sane at the very end,
890 # we insert it back; this is not unified with the __fixup_results() codepaths, since
891 # A) There's no insert analogue B) That's complicated enough as it is. But if we
892 # have problems, we might want to reconsider whether there is some unified way to
893 # do both. Maybe we should just delete all possibly affected ResultChunks and add
894 # them all back at the end?
896 result_chunk = self.__chunks[start_line]
897 restore_result_statement = self.__find_statement_for_result(result_chunk)
898 end_offset = end.get_line_offset()
899 self.__modifying_results = True
900 self.__delete_chunk(result_chunk)
901 self.__modifying_results = False
902 start_line -= 1 + result_chunk.end - result_chunk.start
903 end_line -= 1 + result_chunk.end - result_chunk.start
904 _copy_iter(start, self.get_iter_at_line(start_line))
905 if not start.ends_line():
906 start.forward_to_line_end()
907 _copy_iter(end, self.get_iter_at_line_offset(end_line, end_offset))
909 if end.starts_line() and not start.starts_line() and isinstance(self.__chunks[end_line], ResultChunk):
910 # Merging a ResultChunk onto the end of another chunk; just ignore this; we do have
911 # have to be careful to avoid leaving end pointing to the same place as start, since
912 # we'll then go into an infinite loop
913 new_end = end.copy()
915 new_end.backward_line()
916 if not new_end.ends_line():
917 new_end.forward_to_line_end()
919 if start.compare(new_end) == 0:
920 return
922 end.backward_line()
923 if not end.ends_line():
924 end.forward_to_line_end()
925 end_line -= 1
927 if start.starts_line() and end.starts_line():
928 (first_deleted_line, last_deleted_line) = (start_line, end_line - 1)
929 (new_start, new_end) = (start_line, start_line - 1) # empty
930 last_modified_line = end_line - 1
931 elif start.starts_line():
932 if start_line == end_line:
933 (first_deleted_line, last_deleted_line) = (start_line, start_line - 1) # empty
934 (new_start, new_end) = (start_line, start_line)
935 last_modified_line = start_line
936 else:
937 (first_deleted_line, last_deleted_line) = (start_line, end_line - 1)
938 (new_start, new_end) = (start_line, start_line)
939 last_modified_line = end_line
940 else:
941 (first_deleted_line, last_deleted_line) = (start_line + 1, end_line)
942 (new_start, new_end) = (start_line, start_line)
943 last_modified_line = end_line
945 if not self.__modifying_results:
946 _debug("Deleting range (%s,%s)-(%s,%s)", start.get_line(), start.get_line_offset(), end.get_line(), end.get_line_offset())
947 _debug("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)
949 start_pos = self.__compute_nr_pos_from_iter(start)
950 end_pos = self.__compute_nr_pos_from_iter(end)
951 deleted_text = self.get_slice(start, end)
952 gtk.TextBuffer.do_delete_range(self, start, end)
954 if self.__modifying_results:
955 return
957 self.__freeze_changes()
959 if self.__user_action_count > 0:
960 self.__set_modified(True)
962 self.__undo_stack.append_op(DeleteOp(start_pos, end_pos, deleted_text))
964 result_fixup_state = self.__get_result_fixup_state(new_start, last_modified_line)
966 n_nr_deleted = 0
967 for chunk in self.iterate_chunks(first_deleted_line, last_deleted_line):
968 if not isinstance(chunk, ResultChunk):
969 n_nr_deleted += 1 + min(last_deleted_line, chunk.end) - max(first_deleted_line, chunk.start)
971 n_deleted = 1 + last_deleted_line - first_deleted_line
972 self.__delete_lines(first_deleted_line, last_deleted_line)
974 for chunk in self.iterate_chunks():
975 if chunk.end >= last_deleted_line:
976 chunk.end -= n_deleted;
977 elif chunk.end >= first_deleted_line:
978 chunk.end = first_deleted_line - 1
980 if chunk.start >= last_deleted_line:
981 chunk.start -= n_deleted
982 chunk.nr_start -= n_nr_deleted
984 self.__rescan(new_start, new_end)
986 self.__fixup_results(result_fixup_state, [start, end])
988 if restore_result_statement != None and \
989 self.__chunks[restore_result_statement.start] == restore_result_statement and \
990 self.__find_result(restore_result_statement) == None:
991 start_mark = self.create_mark(None, start, True)
992 end_mark = self.create_mark(None, end, True)
993 result_chunk = self.insert_result(restore_result_statement)
994 _copy_iter(start, self.get_iter_at_mark(start_mark))
995 self.delete_mark(start_mark)
996 _copy_iter(end, self.get_iter_at_mark(end_mark))
997 self.delete_mark(end_mark)
999 # If the cursor ended up in or after the restored result chunk,
1000 # we need to move it before
1001 insert = self.get_iter_at_mark(self.get_insert())
1002 if insert.get_line() >= result_chunk.start:
1003 insert.set_line(result_chunk.start - 1)
1004 if not insert.ends_line():
1005 insert.forward_to_line_end()
1006 self.place_cursor(insert)
1008 self.__thaw_changes()
1009 self.__calculate_pair_location()
1011 _debug("After delete, chunks are %s", self.__chunks)
1013 def calculate(self):
1014 parent = None
1015 have_error = False
1016 for chunk in self.iterate_chunks():
1017 if isinstance(chunk, StatementChunk):
1018 changed = False
1019 if chunk.needs_compile or (chunk.needs_execute and not have_error):
1020 old_result = self.__find_result(chunk)
1021 if old_result:
1022 self.__delete_chunk(old_result)
1024 if chunk.needs_compile:
1025 changed = True
1026 chunk.compile(self)
1027 if chunk.error_message != None:
1028 self.insert_result(chunk)
1030 if chunk.needs_execute and not have_error:
1031 changed = True
1032 chunk.execute(parent)
1033 if chunk.error_message != None:
1034 self.insert_result(chunk)
1035 elif len(chunk.results) > 0:
1036 self.insert_result(chunk)
1038 if chunk.error_message != None:
1039 have_error = True
1041 if changed:
1042 self.emit("chunk-status-changed", chunk)
1044 parent = chunk.statement
1046 _debug("After calculate, chunks are %s", self.__chunks)
1048 def __set_pair_location(self, location):
1049 changed = False
1050 old_location = None
1052 if location == None:
1053 if self.__have_pair:
1054 old_location = self.get_iter_at_mark(self.__pair_mark)
1055 self.__have_pair = False
1056 changed = True
1057 else:
1058 if not self.__have_pair:
1059 self.__have_pair = True
1060 self.move_mark(self.__pair_mark, location)
1061 changed = True
1062 else:
1063 old_location = self.get_iter_at_mark(self.__pair_mark)
1064 if location.compare(old_location) != 0:
1065 self.move_mark(self.__pair_mark, location)
1066 changed = True
1068 if changed:
1069 self.emit('pair-location-changed', old_location, location)
1071 def get_pair_location(self):
1072 if self.__have_pair:
1073 return self.get_iter_at_mark(self.__pair_mark)
1074 else:
1075 return None
1077 def __calculate_pair_location(self):
1078 location = self.get_iter_at_mark(self.get_insert())
1080 # GTK+-2.10 has fractionally-more-efficient buffer.get_has_selection()
1081 selection_bound = self.get_iter_at_mark(self.get_selection_bound())
1082 if location.compare(selection_bound) != 0:
1083 self.__set_pair_location(None)
1084 return
1086 location = self.get_iter_at_mark(self.get_insert())
1088 line = location.get_line()
1089 chunk = self.__chunks[line]
1090 if not isinstance(chunk, StatementChunk):
1091 self.__set_pair_location(None)
1092 return
1094 if location.starts_line():
1095 self.__set_pair_location(None)
1096 return
1098 previous = location.copy()
1099 previous.backward_char()
1100 pair_line, pair_start = chunk.tokenized.get_pair_location(line - chunk.start, previous.get_line_index())
1102 if pair_line == None:
1103 self.__set_pair_location(None)
1104 return
1106 pair_iter = self.get_iter_at_line_index(chunk.start + pair_line, pair_start)
1107 self.__set_pair_location(pair_iter)
1109 def do_mark_set(self, location, mark):
1110 try:
1111 gtk.TextBuffer.do_mark_set(self, location, mark)
1112 except NotImplementedError:
1113 # the default handler for ::mark-set was added in GTK+-2.10
1114 pass
1116 if mark != self.get_insert() and mark != self.get_selection_bound():
1117 return
1119 self.__calculate_pair_location()
1121 def get_chunk(self, line_index):
1122 return self.__chunks[line_index]
1124 def undo(self):
1125 self.__undo_stack.undo()
1127 def redo(self):
1128 self.__undo_stack.redo()
1130 def __get_chunk_bounds(self, chunk):
1131 start = self.get_iter_at_line(chunk.start)
1132 end = self.get_iter_at_line(chunk.end)
1133 if not end.ends_line():
1134 end.forward_to_line_end()
1135 return start, end
1137 def copy_as_doctests(self, clipboard):
1138 bounds = self.get_selection_bounds()
1139 if bounds == ():
1140 start, end = self.get_iter_at_mark(self.get_insert())
1141 else:
1142 start, end = bounds
1144 result = ""
1145 for chunk in self.iterate_chunks(start.get_line(), end.get_line()):
1146 chunk_text = self.get_text(*self.__get_chunk_bounds(chunk))
1148 if isinstance(chunk, ResultChunk) or isinstance(chunk, BlankChunk):
1149 if chunk.end == len(self.__chunks) - 1:
1150 result += chunk_text
1151 else:
1152 result += chunk_text + "\n"
1153 else:
1154 first = True
1155 for line in chunk_text.split("\n"):
1156 if isinstance(chunk, StatementChunk) and not first:
1157 result += "... " + line + "\n"
1158 else:
1159 result += ">>> " + line + "\n"
1160 first = False
1162 clipboard.set_text(result)
1164 def __fontify_statement_lines(self, chunk, changed_lines):
1165 iter = self.get_iter_at_line(chunk.start)
1166 i = 0
1167 for l in changed_lines:
1168 while i < l:
1169 iter.forward_line()
1170 i += 1
1171 end = iter.copy()
1172 if not end.ends_line():
1173 end.forward_to_line_end()
1174 self.remove_all_tags(iter, end)
1176 end = iter.copy()
1177 for token_type, start_index, end_index, _ in chunk.tokenized.get_tokens(l):
1178 tag = self.__fontify_tags[token_type]
1179 if tag != None:
1180 iter.set_line_index(start_index)
1181 end.set_line_index(end_index)
1182 self.apply_tag(tag, iter, end)
1184 def __apply_tag_to_chunk(self, tag, chunk, remove_old=False):
1185 start, end = self.__get_chunk_bounds(chunk)
1186 if remove_old:
1187 self.remove_all_tags(start, end)
1188 self.apply_tag(tag, start, end)
1190 def __remove_tag_from_chunk(self, tag, chunk):
1191 start, end = self.__get_chunk_bounds(chunk)
1192 self.remove_tag(tag, start, end)
1194 def insert_result(self, chunk):
1195 self.__modifying_results = True
1196 location = self.get_iter_at_line(chunk.end)
1197 if not location.ends_line():
1198 location.forward_to_line_end()
1200 if chunk.error_message:
1201 results = [ chunk.error_message ]
1202 else:
1203 results = chunk.results
1205 # We don't want to move the insert cursor in the common case of
1206 # inserting a result right at the insert cursor
1207 if location.compare(self.get_iter_at_mark(self.get_insert())) == 0:
1208 saved_insert = self.create_mark(None, location, True)
1209 else:
1210 saved_insert = None
1212 for result in results:
1213 if isinstance(result, basestring):
1214 self.insert(location, "\n" + result)
1215 elif isinstance(result, WarningResult):
1216 start_mark = self.create_mark(None, location, True)
1217 self.insert(location, "\n" + result.message)
1218 start = self.get_iter_at_mark(start_mark)
1219 self.delete_mark(start_mark)
1220 self.apply_tag(self.__warning_tag, start, location)
1221 elif isinstance(result, HelpResult):
1222 self.insert(location, "\n")
1223 start_mark = self.create_mark(None, location, True)
1224 doc_format.insert_docs(self, location, result.arg, self.__bold_tag)
1225 start = self.get_iter_at_mark(start_mark)
1226 self.delete_mark(start_mark)
1227 self.apply_tag(self.__help_tag, start, location)
1228 elif isinstance(result, CustomResult):
1229 self.insert(location, "\n")
1230 anchor = self.create_child_anchor(location)
1231 self.emit("add-custom-result", result, anchor)
1233 self.__modifying_results = False
1234 n_inserted = location.get_line() - chunk.end
1236 result_chunk = ResultChunk(chunk.end + 1, chunk.end + n_inserted)
1237 self.__compute_nr_start(result_chunk)
1238 self.__insert_lines(chunk.end + 1, n_inserted, chunk=result_chunk)
1240 self.__apply_tag_to_chunk(self.__result_tag, result_chunk)
1242 if chunk.error_message:
1243 self.__apply_tag_to_chunk(self.__error_tag, result_chunk)
1245 for chunk in self.iterate_chunks(result_chunk.end + 1):
1246 chunk.start += n_inserted
1247 chunk.end += n_inserted
1249 if saved_insert != None:
1250 self.place_cursor(self.get_iter_at_mark(saved_insert))
1251 self.delete_mark(saved_insert)
1253 return result_chunk
1255 def set_filename_and_modified(self, filename, modified):
1256 "Update the filename and modified fields, emitting 'filename-changed' and 'code-modified-changed' signals as appropriate."
1257 # This method is needed by rox_ui, because saving to a file and updating the filename happen separately in the saving process.
1259 filename_changed = filename != self.filename
1260 modified_changed = modified != self.code_modified
1262 if not (filename_changed or modified_changed):
1263 return
1265 self.filename = filename
1266 self.code_modified = modified
1268 if filename_changed:
1269 self.emit('filename-changed')
1271 if modified_changed:
1272 self.emit('code-modified-changed')
1274 def __set_modified(self, modified):
1275 if modified == self.code_modified:
1276 return
1278 self.code_modified = modified
1279 self.emit('code-modified-changed')
1281 def __do_clear(self):
1282 # This is actually working pretty much coincidentally, since the Delete
1283 # code wasn't really written with non-interactive deletes in mind, and
1284 # when there are ResultChunk present, a non-interactive delete will
1285 # use ranges including them. But the logic happens to work out.
1287 self.delete(self.get_start_iter(), self.get_end_iter())
1289 def clear(self):
1290 self.__do_clear()
1291 self.set_filename_and_modified(None, False)
1293 # This prevents redoing New, but we need some more work to enable that
1294 self.__undo_stack.clear()
1296 def load(self, filename):
1297 f = open(filename)
1298 text = f.read()
1299 f.close()
1301 self.__do_clear()
1302 self.set_filename_and_modified(filename, False)
1303 self.insert(self.get_start_iter(), text)
1304 self.__undo_stack.clear()
1306 def save(self, filename=None):
1307 if filename == None:
1308 if self.filename == None:
1309 raise ValueError("No current or specified filename")
1311 filename = self.filename
1313 # TODO: The atomic-save implementation here is Unix-specific and won't work on Windows
1314 tmpname = filename + ".tmp"
1316 # We use binary mode, since we don't want to munge line endings to the system default
1317 # on a load-save cycle
1318 f = open(tmpname, "wb")
1320 success = False
1321 try:
1322 for chunk_text in self.iterate_text():
1323 f.write(chunk_text)
1325 f.close()
1326 os.rename(tmpname, filename)
1327 success = True
1329 self.set_filename_and_modified(filename, False)
1330 finally:
1331 if not success:
1332 f.close()
1333 os.remove(tmpname)
1335 def __get_last_scope(self, chunk):
1336 # Get the last result scope we have that precedes the specified chunk
1338 scope = None
1339 line = chunk.start - 1
1340 while line >= 0:
1341 previous_chunk = self.__chunks[line]
1343 # We intentionally don't check "needs_execute" ... if there is a result scope,
1344 # it's fair game for completion/help, even if it's old
1345 if isinstance(previous_chunk, StatementChunk) and previous_chunk.statement != None and previous_chunk.statement.result_scope != None:
1346 return previous_chunk.statement.result_scope
1347 break
1349 line = previous_chunk.start - 1
1351 return self.global_scope
1355 def find_completions(self):
1356 """Returns a list of possible completions at insertion cursor position.
1358 Each element in the returned list is a tuple of (display_form,
1359 text_to_insert, object_completed_to)' where
1360 object_completed_to can be used to determine the type of the
1361 completion or get docs about it.
1365 insert = self.get_iter_at_mark(self.get_insert())
1366 chunk = self.__chunks[insert.get_line()]
1367 if not isinstance(chunk, StatementChunk) and not isinstance(chunk, BlankChunk):
1368 return []
1370 scope = self.__get_last_scope(chunk)
1372 if isinstance(chunk, StatementChunk):
1373 return chunk.tokenized.find_completions(insert.get_line() - chunk.start,
1374 insert.get_line_index(),
1375 scope)
1376 else:
1377 # A BlankChunk Create a dummy TokenizedStatement to get the completions
1378 # appropriate for the start of a line
1379 ts = TokenizedStatement()
1380 ts.set_lines([''])
1381 return ts.find_completions(0, 0, scope)
1383 def get_object_at_location(self, location, include_adjacent=False):
1384 """Find the object at a particular location within the buffer
1386 include_adjacent -- if False, then location identifies a character in the buffer. If True,
1387 then location identifies a position between characters, and symbols before or after that
1388 position are included.
1390 Returns a tuple of (location, tuple_start_iter, tuple_end_iter) or (None, None, None)
1394 chunk = self.__chunks[location.get_line()]
1395 if not isinstance(chunk, StatementChunk):
1396 return None, None, None
1398 if chunk.statement != None and chunk.statement.result_scope != None:
1399 result_scope = chunk.statement.result_scope
1400 else:
1401 result_scope = None
1403 obj, start_line, start_index, end_line, end_index = \
1404 chunk.tokenized.get_object_at_location(location.get_line() - chunk.start,
1405 location.get_line_index(),
1406 self.__get_last_scope(chunk),
1407 result_scope, include_adjacent)
1409 if obj == None:
1410 return None, None, None
1412 start_iter = self.get_iter_at_line_index(chunk.start + start_line, start_index)
1413 end_iter = self.get_iter_at_line_index(chunk.start + end_line, end_index)
1415 return obj, start_iter, end_iter
1417 if __name__ == '__main__':
1418 if "-d" in sys.argv:
1419 logging.basicConfig(level=logging.DEBUG, format="DEBUG: %(message)s")
1421 import stdout_capture
1422 stdout_capture.init()
1424 S = StatementChunk
1425 B = BlankChunk
1426 C = CommentChunk
1427 R = ResultChunk
1429 def compare(l1, l2):
1430 if len(l1) != len(l2):
1431 return False
1433 for i in xrange(0, len(l1)):
1434 e1 = l1[i]
1435 e2 = l2[i]
1437 if type(e1) != type(e2) or e1.start != e2.start or e1.end != e2.end:
1438 return False
1440 return True
1442 buffer = ShellBuffer(Notebook())
1444 def validate_nr_start():
1445 n_nr = 0
1446 for chunk in buffer.iterate_chunks():
1447 if chunk.nr_start != n_nr:
1448 raise AssertionError("nr_start for chunk %s should have been %d but is %d" % (chunk, n_nr, chunk.nr_start))
1449 assert(chunk.nr_start == n_nr)
1450 if not isinstance(chunk, ResultChunk):
1451 n_nr += 1 + chunk.end - chunk.start
1453 def expect(expected):
1454 chunks = [ x for x in buffer.iterate_chunks() ]
1455 if not compare(chunks, expected):
1456 raise AssertionError("\nGot:\n %s\nExpected:\n %s" % (chunks, expected))
1457 validate_nr_start()
1459 def expect_text(expected, start_line=None, start_offset=None, end_line=None, end_offset=None):
1460 if start_offset != None:
1461 i = buffer.get_iter_at_line_offset(start_line, start_offset)
1462 else:
1463 i = None
1465 if end_offset != None:
1466 j = buffer.get_iter_at_line_offset(end_line, end_offset)
1467 else:
1468 j = None
1470 text = buffer.get_public_text(i, j)
1471 if (text != expected):
1472 raise AssertionError("\nGot:\n '%s'\nExpected:\n '%s'" % (text, expected))
1474 def insert(line, offset, text):
1475 i = buffer.get_iter_at_line_offset(line, offset)
1476 buffer.insert_interactive(i, text, True)
1478 def delete(start_line, start_offset, end_line, end_offset):
1479 i = buffer.get_iter_at_line_offset(start_line, start_offset)
1480 j = buffer.get_iter_at_line_offset(end_line, end_offset)
1481 buffer.delete_interactive(i, j, True)
1483 def clear():
1484 buffer.clear()
1486 # Basic operation
1487 insert(0, 0, "1\n\n#2\ndef a():\n 3")
1488 expect([S(0,0), B(1,1), C(2,2), S(3,4)])
1490 clear()
1491 expect([B(0,0)])
1493 # Turning a statement into a continuation line
1494 insert(0, 0, "1 \\\n+ 2\n")
1495 insert(1, 0, " ")
1496 expect([S(0,1), B(2,2)])
1498 # Calculation resulting in result chunks
1499 insert(2, 0, "3\n")
1500 buffer.calculate()
1501 expect([S(0,1), R(2,2), S(3,3), R(4,4), B(5,5)])
1503 # Check that splitting a statement with a delete results in the
1504 # result chunk being moved to the last line of the first half
1505 delete(1, 0, 1, 1)
1506 expect([S(0,0), R(1,1), S(2,2), S(3,3), R(4,4), B(5,5)])
1508 # Editing a continuation line, while leaving it a continuation
1509 clear()
1511 insert(0, 0, "1\\\n + 2\\\n + 3")
1512 delete(1, 0, 1, 1)
1513 expect([S(0,2)])
1515 # Editing a line with an existing error chunk to fix the error
1516 clear()
1518 insert(0, 0, "a\na=2")
1519 buffer.calculate()
1521 insert(0, 0, "2")
1522 delete(0, 1, 0, 2)
1523 buffer.calculate()
1524 expect([S(0,0), R(1,1), S(2,2)])
1526 # Deleting an entire continuation line
1527 clear()
1529 insert(0, 0, "for i in (1,2):\n print i\n print i + 1\n")
1530 expect([S(0,2), B(3,3)])
1531 delete(1, 0, 2, 0)
1532 expect([S(0,1), B(2,2)])
1534 # Test an attempt to join a ResultChunk onto a previous chunk; should ignore
1535 clear()
1537 insert(0, 0, "1\n");
1538 buffer.calculate()
1539 expect([S(0,0), R(1,1), B(2,2)])
1540 delete(0, 1, 1, 0)
1541 expect_text("1\n");
1543 # Test an attempt to join a chunk onto a previous ResultChunk, should move
1544 # the ResultChunk and do the modification
1545 clear()
1547 insert(0, 0, "1\n2\n");
1548 buffer.calculate()
1549 expect([S(0,0), R(1,1), S(2,2), R(3,3), B(4,4)])
1550 delete(1, 1, 2, 0)
1551 expect([S(0,0), R(1,1), B(2,2)])
1552 expect_text("12\n");
1554 # Test inserting random text inside a result chunk, should ignore
1555 clear()
1557 insert(0, 0, "1\n2");
1558 buffer.calculate()
1559 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
1560 insert(1, 0, "foo")
1561 expect_text("1\n2");
1562 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
1564 # Test inserting a newline at the end of a result chunk, should create
1565 # a new line
1566 insert(1, 1, "\n")
1567 expect_text("1\n\n2");
1568 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4)])
1570 # Same, at the end of the buffer
1571 insert(4, 1, "\n")
1572 expect_text("1\n\n2\n");
1573 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4), B(5,5)])
1575 # Try undoing these insertions
1576 buffer.undo()
1577 expect_text("1\n\n2");
1578 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4)])
1580 buffer.undo()
1581 expect_text("1\n2");
1582 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
1584 # Calculation resulting in a multi-line result change
1585 clear()
1587 insert(0, 0, "for i in range(0, 10): print i")
1588 buffer.calculate()
1589 expect([S(0, 0), R(1, 10)])
1591 # Test that commenting out a line marks subsequent lines for recalculation
1592 clear()
1594 insert(0, 0, "a = 1\na = 2\na")
1595 buffer.calculate()
1596 insert(1, 0, "#")
1597 assert buffer.get_chunk(2).needs_execute
1599 # Test deleting a range containing both results and statements
1601 clear()
1603 insert(0, 0, "1\n2\n3\n4\n")
1604 buffer.calculate()
1605 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)])
1607 delete(2, 0, 5, 0)
1608 expect([S(0,0), R(1,1), S(2,2), R(3,3), B(4,4)])
1610 # Inserting an entire new statement in the middle
1611 insert(2, 0, "2.5\n")
1612 expect([S(0,0), R(1,1), S(2,2), S(3,3), R(4,4), B(5,5)])
1613 buffer.calculate()
1614 expect([S(0,0), R(1,1), S(2,2), R(3, 3), S(4, 4), R(5,5), B(6,6)])
1616 # Check that inserting a blank line at the beginning of a statement leaves
1617 # the result behind
1618 insert(2, 0, "\n")
1619 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)])
1621 # Test deleting a range including a result and joining two statements
1622 clear()
1623 insert(0, 0, "12\n34")
1624 buffer.calculate()
1625 delete(0, 1, 2, 1)
1626 expect_text("14")
1628 # Test a deletion that splits the buffer into two (invalid) pieces
1629 clear()
1630 insert(0, 0, "try:\n a = 1\nfinally:\n print 'Done'")
1631 buffer.calculate()
1632 expect([S(0,3), R(4,4)])
1633 delete(2, 7, 2, 8)
1634 buffer.calculate()
1635 expect([S(0,1), R(2,2), S(3,4), R(5,5)])
1637 # Try an insertion that combines the two pieces and makes them valid
1638 # again (combining across the error result chunk)
1639 insert(3, 7, ":")
1640 buffer.calculate()
1641 expect([S(0,3), R(4,4)])
1643 # Undo tests
1644 clear()
1646 insert(0, 0, "1")
1647 buffer.undo()
1648 expect_text("")
1649 buffer.redo()
1650 expect_text("1")
1652 # Undoing insertion of a newline
1653 clear()
1655 insert(0, 0, "1 ")
1656 insert(0, 1, "\n")
1657 buffer.calculate()
1658 buffer.undo()
1659 expect_text("1 ")
1661 # Test the "pruning" behavior of modifications after undos
1662 clear()
1664 insert(0, 0, "1")
1665 buffer.undo()
1666 expect_text("")
1667 insert(0, 0, "2")
1668 buffer.redo() # does nothing
1669 expect_text("2")
1670 insert(0, 0, "2\n")
1672 # Test coalescing consecutive inserts
1673 clear()
1675 insert(0, 0, "1")
1676 insert(0, 1, "2")
1677 buffer.undo()
1678 expect_text("")
1680 # Test grouping of multiple undos by user actions
1681 clear()
1683 insert(0, 0, "1")
1684 buffer.begin_user_action()
1685 delete(0, 0, 0, 1)
1686 insert(0, 0, "2")
1687 buffer.end_user_action()
1688 buffer.undo()
1689 expect_text("1")
1690 buffer.redo()
1691 expect_text("2")
1693 # Make sure that coalescing doesn't coalesce one user action with
1694 # only part of another
1695 clear()
1697 insert(0, 0, "1")
1698 buffer.begin_user_action()
1699 insert(0, 1, "2")
1700 delete(0, 0, 0, 1)
1701 buffer.end_user_action()
1702 buffer.undo()
1703 expect_text("1")
1704 buffer.redo()
1705 expect_text("2")
1707 # Test an undo of an insert that caused insertion of result chunks
1708 clear()
1710 insert(0, 0, "2\n")
1711 expect([S(0,0), B(1,1)])
1712 buffer.calculate()
1713 expect([S(0,0), R(1,1), B(2,2)])
1714 insert(0, 0, "1\n")
1715 buffer.calculate()
1716 buffer.undo()
1717 expect([S(0,0), R(1,1), B(2,2)])
1718 expect_text("2\n")
1720 # Tests of get_public_text()
1721 clear()
1722 insert(0, 0, "12\n34\n56")
1723 buffer.calculate()
1725 expect_text("12\n34\n56", 0, 0, 5, 2)
1726 expect_text("4\n5", 2, 1, 4, 1)
1728 # within a single result get_public_text() *does* include the text of the result
1729 expect_text("1", 1, 0, 1, 1)
1732 # Try writing to a file, and reading it back
1734 import tempfile, os
1736 clear()
1737 expect([B(0,0)])
1739 SAVE_TEST = """a = 1
1741 # A comment
1743 b = 2"""
1745 insert(0, 0, SAVE_TEST)
1746 buffer.calculate()
1748 handle, fname = tempfile.mkstemp(".txt", "shell_buffer")
1749 os.close(handle)
1751 try:
1752 buffer.save(fname)
1753 f = open(fname, "r")
1754 saved = f.read()
1755 f.close()
1757 if saved != SAVE_TEST:
1758 raise AssertionError("Got '%s', expected '%s'", saved, SAVE_TEST)
1760 buffer.load(fname)
1761 buffer.calculate()
1763 expect([S(0,0), S(1,1), R(2,2), C(3,3), B(4,4), S(5,5)])
1764 finally:
1765 os.remove(fname)
1767 clear()
1768 expect([B(0,0)])