11 from notebook
import Notebook
, HelpResult
12 from statement
import Statement
, ExecutionError
, WarningResult
13 from worksheet
import Worksheet
14 from custom_result
import CustomResult
16 from tokenized_statement
import TokenizedStatement
17 from undo_stack
import UndoStack
, InsertOp
, DeleteOp
19 # See comment in iter_copy_from.py
21 gtk
.TextIter
.copy_from
22 def _copy_iter(dest
, src
):
24 except AttributeError:
25 from iter_copy_from
import iter_copy_from
as _copy_iter
27 _debug
= logging
.getLogger("ShellBuffer").debug
30 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
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
40 self
.tokenized
= TokenizedStatement()
42 self
.needs_compile
= False
43 self
.needs_execute
= False
48 self
.error_message
= None
49 self
.error_line
= None
50 self
.error_offset
= None
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:
60 self
.needs_compile
= True
61 self
.needs_execute
= False
67 def mark_for_execute(self
):
68 if self
.statement
== None or self
.needs_execute
:
71 self
.needs_execute
= True
74 def compile(self
, worksheet
):
75 if self
.statement
!= None:
78 self
.needs_compile
= False
82 self
.error_message
= None
83 self
.error_line
= None
84 self
.error_offset
= None
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
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
117 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
120 self
.nr_start
= nr_start
124 return "BlankChunk(%d,%d)" % (self
.start
, self
.end
)
127 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
130 self
.nr_start
= nr_start
134 return "CommentChunk(%d,%d)" % (self
.start
, self
.end
)
137 def __init__(self
, start
=-1, end
=-1, nr_start
=-1):
140 self
.nr_start
= nr_start
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
:
153 class ShellBuffer(gtk
.TextBuffer
, Worksheet
):
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
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",
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"),
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
)
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
)
250 self
.__apply
_tag
_to
_chunk
(self
.__recompute
_tag
, result
)
252 self
.emit("chunk-status-changed", chunk
)
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")
262 if chunk
in self
.__changed
_chunks
:
263 if changed_lines
!= None:
264 old
= self
.__changed
_chunks
[chunk
]
266 self
.__changed
_chunks
[chunk
] = changed_lines
268 s
= set(changed_lines
)
270 self
.__changed
_chunks
[chunk
] = sorted(s
)
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)
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
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
):
297 chunk
.line_count
-= 1
298 if chunk
.line_count
== 0:
300 del self
.__changed
_chunks
[chunk
]
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
))
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
):
340 chunk_before
= self
.__chunks
[chunk
.start
- 1]
341 if isinstance(chunk_before
, ResultChunk
):
342 chunk
.nr_start
= chunk_before
.nr_start
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
])
352 for i
in xrange(chunk_start
, statement_end
+ 1):
353 if isinstance(self
.__chunks
[i
], StatementChunk
):
354 old_statement
= self
.__chunks
[i
]
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
)
372 chunk
= StatementChunk()
373 changed_lines
= chunk
.set_lines(chunk_lines
)
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
]
390 chunk
= self
.__chunks
[i
- 1]
395 # a ResultChunk Must be in the before-start portion, nothing needs doing
397 elif BLANK
.match(line
):
398 if not isinstance(chunk
, BlankChunk
):
401 self
.__compute
_nr
_start
(chunk
)
403 self
.__set
_line
(i
, chunk
, lines
[i
- chunk_start
])
404 elif COMMENT
.match(line
):
405 if not isinstance(chunk
, CommentChunk
):
406 chunk
= CommentChunk()
408 self
.__compute
_nr
_start
(chunk
)
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
]
420 old_text
= self
.__lines
[rescan_start
]
421 i
= self
.get_iter_at_line(rescan_start
)
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
):
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:
443 chunk_start
= rescan_start
444 statement_end
= rescan_start
- 1
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
]
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
)
469 self
.__assign
_lines
(chunk_start
, chunk_lines
, statement_end
)
472 chunk_lines
= [line_text
]
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
:
484 chunk
= self
.__chunks
[start_line
]
485 while chunk
== None and start_line
< end_line
:
487 chunk
= self
.__chunks
[start_line
]
492 last_chunk
= self
.__chunks
[end_line
]
493 while last_chunk
== None:
495 last_chunk
= self
.__chunks
[end_line
]
499 if chunk
== last_chunk
:
503 chunk
= self
.__chunks
[line
]
506 chunk
= self
.__chunks
[line
]
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()
515 def iterate_text(self
, start
=None, end
=None):
519 start
= self
.get_start_iter()
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
)
533 iter = self
.get_iter_at_line(chunk
.start
)
536 next_line
= chunk
.end
+ 1
537 if next_line
< len(self
.__chunks
):
538 next_chunk
= self
.__chunks
[chunk
.end
+ 1]
541 while next
.get_line() <= chunk
.end
:
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
):
552 if not next
.ends_line():
553 next
.forward_to_line_end()
556 if not isinstance(chunk
, ResultChunk
):
557 chunk_start
, chunk_end
= iter, next
558 if chunk
== start_chunk
:
563 if chunk
== end_chunk
:
568 yield self
.get_slice(chunk_start
, chunk_end
)
572 if chunk
== end_chunk
or next_chunk
== None:
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)
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
):
610 nr_line
, offset
= nr_pos
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()
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):
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()):
663 if not (text
.startswith("\r") or text
.startswith("\n")):
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
:
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
)
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
)
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
)
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
)
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
):
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
:
765 elif c
.end
>= chunk
.start
:
766 c
.end
= chunk
.start
- 1
768 if c
.start
>= chunk
.end
:
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
):
779 elif isinstance(chunk
, StatementChunk
):
782 def __find_statement_for_result(self
, result_chunk
):
783 line
= result_chunk
.start
- 1
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
]
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
813 elif isinstance(self
.__chunks
[i
], StatementChunk
) and self
.__chunks
[i
].start
== i
:
818 def __fixup_results(self
, state
, revalidate_iters
):
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
:
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
:
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
):
841 if not (move_before
or delete_after
or move_after
):
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
)
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
)
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
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:
923 if not end
.ends_line():
924 end
.forward_to_line_end()
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
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
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
:
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
)
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
):
1016 for chunk
in self
.iterate_chunks():
1017 if isinstance(chunk
, StatementChunk
):
1019 if chunk
.needs_compile
or (chunk
.needs_execute
and not have_error
):
1020 old_result
= self
.__find
_result
(chunk
)
1022 self
.__delete
_chunk
(old_result
)
1024 if chunk
.needs_compile
:
1027 if chunk
.error_message
!= None:
1028 self
.insert_result(chunk
)
1030 if chunk
.needs_execute
and not have_error
:
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:
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
):
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
1058 if not self
.__have
_pair
:
1059 self
.__have
_pair
= True
1060 self
.move_mark(self
.__pair
_mark
, location
)
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
)
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
)
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)
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)
1094 if location
.starts_line():
1095 self
.__set
_pair
_location
(None)
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)
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
):
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
1116 if mark
!= self
.get_insert() and mark
!= self
.get_selection_bound():
1119 self
.__calculate
_pair
_location
()
1121 def get_chunk(self
, line_index
):
1122 return self
.__chunks
[line_index
]
1125 self
.__undo
_stack
.undo()
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()
1137 def copy_as_doctests(self
, clipboard
):
1138 bounds
= self
.get_selection_bounds()
1140 start
, end
= self
.get_iter_at_mark(self
.get_insert())
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
1152 result
+= chunk_text
+ "\n"
1155 for line
in chunk_text
.split("\n"):
1156 if isinstance(chunk
, StatementChunk
) and not first
:
1157 result
+= "... " + line
+ "\n"
1159 result
+= ">>> " + line
+ "\n"
1162 clipboard
.set_text(result
)
1164 def __fontify_statement_lines(self
, chunk
, changed_lines
):
1165 iter = self
.get_iter_at_line(chunk
.start
)
1167 for l
in changed_lines
:
1172 if not end
.ends_line():
1173 end
.forward_to_line_end()
1174 self
.remove_all_tags(iter, end
)
1177 for token_type
, start_index
, end_index
, _
in chunk
.tokenized
.get_tokens(l
):
1178 tag
= self
.__fontify
_tags
[token_type
]
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
)
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
]
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)
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
)
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
):
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
:
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())
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
):
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")
1322 for chunk_text
in self
.iterate_text():
1326 os
.rename(tmpname
, filename
)
1329 self
.set_filename_and_modified(filename
, False)
1335 def __get_last_scope(self
, chunk
):
1336 # Get the last result scope we have that precedes the specified chunk
1339 line
= chunk
.start
- 1
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
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
):
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(),
1377 # A BlankChunk Create a dummy TokenizedStatement to get the completions
1378 # appropriate for the start of a line
1379 ts
= TokenizedStatement()
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
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
)
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()
1429 def compare(l1
, l2
):
1430 if len(l1
) != len(l2
):
1433 for i
in xrange(0, len(l1
)):
1437 if type(e1
) != type(e2
) or e1
.start
!= e2
.start
or e1
.end
!= e2
.end
:
1442 buffer = ShellBuffer(Notebook())
1444 def validate_nr_start():
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
))
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
)
1465 if end_offset
!= None:
1466 j
= buffer.get_iter_at_line_offset(end_line
, end_offset
)
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)
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)])
1493 # Turning a statement into a continuation line
1494 insert(0, 0, "1 \\\n+ 2\n")
1496 expect([S(0,1), B(2,2)])
1498 # Calculation resulting in result chunks
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
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
1511 insert(0, 0, "1\\\n + 2\\\n + 3")
1515 # Editing a line with an existing error chunk to fix the error
1518 insert(0, 0, "a\na=2")
1524 expect([S(0,0), R(1,1), S(2,2)])
1526 # Deleting an entire continuation line
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)])
1532 expect([S(0,1), B(2,2)])
1534 # Test an attempt to join a ResultChunk onto a previous chunk; should ignore
1537 insert(0, 0, "1\n");
1539 expect([S(0,0), R(1,1), B(2,2)])
1543 # Test an attempt to join a chunk onto a previous ResultChunk, should move
1544 # the ResultChunk and do the modification
1547 insert(0, 0, "1\n2\n");
1549 expect([S(0,0), R(1,1), S(2,2), R(3,3), B(4,4)])
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
1557 insert(0, 0, "1\n2");
1559 expect([S(0,0), R(1,1), S(2,2), R(3,3)])
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
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
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
1577 expect_text("1\n\n2");
1578 expect([S(0,0), R(1,1), B(2,2), S(3,3), R(4,4)])
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
1587 insert(0, 0, "for i in range(0, 10): print i")
1589 expect([S(0, 0), R(1, 10)])
1591 # Test that commenting out a line marks subsequent lines for recalculation
1594 insert(0, 0, "a = 1\na = 2\na")
1597 assert buffer.get_chunk(2).needs_execute
1599 # Test deleting a range containing both results and statements
1603 insert(0, 0, "1\n2\n3\n4\n")
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)])
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)])
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
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
1623 insert(0, 0, "12\n34")
1628 # Test a deletion that splits the buffer into two (invalid) pieces
1630 insert(0, 0, "try:\n a = 1\nfinally:\n print 'Done'")
1632 expect([S(0,3), R(4,4)])
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)
1641 expect([S(0,3), R(4,4)])
1652 # Undoing insertion of a newline
1661 # Test the "pruning" behavior of modifications after undos
1668 buffer.redo() # does nothing
1672 # Test coalescing consecutive inserts
1680 # Test grouping of multiple undos by user actions
1684 buffer.begin_user_action()
1687 buffer.end_user_action()
1693 # Make sure that coalescing doesn't coalesce one user action with
1694 # only part of another
1698 buffer.begin_user_action()
1701 buffer.end_user_action()
1707 # Test an undo of an insert that caused insertion of result chunks
1711 expect([S(0,0), B(1,1)])
1713 expect([S(0,0), R(1,1), B(2,2)])
1717 expect([S(0,0), R(1,1), B(2,2)])
1720 # Tests of get_public_text()
1722 insert(0, 0, "12\n34\n56")
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
1739 SAVE_TEST
= """a = 1
1745 insert(0, 0, SAVE_TEST
)
1748 handle
, fname
= tempfile
.mkstemp(".txt", "shell_buffer")
1753 f
= open(fname
, "r")
1757 if saved
!= SAVE_TEST
:
1758 raise AssertionError("Got '%s', expected '%s'", saved
, SAVE_TEST
)
1763 expect([S(0,0), S(1,1), R(2,2), C(3,3), B(4,4), S(5,5)])