コード例 #1
0
ファイル: chunks.py プロジェクト: lamby/pkg-reinteract
class Chunk(object):

    """
    A chunk is a series of consecutive lines in a Worksheet treated as a unit.
    The Chunk class is a base class for different types of chunks. It contains
    basic functionality for tracking a [start,end) range, and tracking what
    lines withinthe chunk have changed.
    """

    def __init__(self, start=-1, end=-1):
        self.start = start
        self.end = end
        self.changes = ChangeRange()
        self.newly_inserted = True

    def set_range(self, start, end):
        if start < self.start:
            self.changes.insert(0, self.start - start)
            self.start = start
        if end > self.end:
            self.changes.insert(self.end -self.start, self.start - start)
            self.end = end
        if start > self.start:
            self.changes.delete_range(0, start - self.start)
            self.start = start
        if end < self.end:
            self.changes.delete_range(end - self.start, self.end - self.start)
            self.end = end

    def change_line(self, line):
        self.changes.change(line - self.start, line + 1 - self.start)

    def change_lines(self, start, end):
        self.changes.change(start - self.start, end - self.start)

    def insert_lines(self, pos, count):
        self.changes.insert(pos - self.start, count)
        self.end += count

    def delete_lines(self, start, end):
        self.changes.delete_range(start - self.start, end - self.start)
        # Note: deleting everything gives [end,end], which is legitimate
        # but maybe a little surprising. Doesn't matter for us.
        if start == self.start:
            self.start = end
        else:
            self.end -= (end - start)
コード例 #2
0
ファイル: worksheet.py プロジェクト: johnrizzo1/reinteract
class Worksheet(gobject.GObject):
    __gsignals__ = {
        # text-* are emitted before we fix up our internal state, so what can be done
        # in them are limited. They are meant for keeping a UI in sync with the internal
        # state.
        'text-inserted': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (int, int, str)),
        'text-deleted': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (int, int, int, int)),
        'lines-inserted': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (int, int)),
        'lines-deleted': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (int, int)),
        'chunk-inserted': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        # Chunk changed is emitted when the text or tokenization of a chunk
        # changes. Note that "changes" here specifically includes being
        # replaced by identical text, so if I have the two chunks
        #
        #  if
        #  if
        #
        # And I delete the from the first 'i' to the second f, the first
        # chunk is considered to change, even though it's text remains 'if'.
        # This is because text in a buffering that is shadowing us may
        # be tagged with fonts/styles.
        #
        'chunk-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
        'chunk-deleted': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        'chunk-status-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        'chunk-results-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        # This is only for the convenience of the undo stack; otherwise we ignore cursor position
        'place-cursor': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (int, int))
    }

    def __init__(self, notebook, edit_only=False):
        gobject.GObject.__init__(self)

        self.notebook = notebook
        self.edit_only = edit_only
        self.__file = None
        self.__filename = None
        self.__code_modified = False

        self.global_scope = {}
        notebook.setup_globals(self.global_scope)
        exec _DEFINE_GLOBALS in self.global_scope

        self.__lines = [""]
        self.__chunks = [BlankChunk(0,1)]

        # There's quite a bit of complexity knowing when a change to lines changes
        # adjacent chunks. We use a simple and slightly inefficient algorithm for this
        # and just scan everything that might have changed. But we don't want typing
        # within a line to cause an unlimited rescan, so we keep track if the only
        # changes we've made are inserting/deleting within a line without changing
        # that lines class
        self.__changes = ChangeRange()
        self.__scan_adjacent = False

        self.__changed_chunks = set()
        self.__deleted_chunks = set()
        self.__freeze_changes_count = 0
        self.__user_action_count = 0

        self.__undo_stack = UndoStack(self)

        notebook._add_worksheet(self)

    def do_import(self, name, globals, locals, fromlist, level):
        __import__(self, name, globals, locals, fromlist, level)

    def iterate_chunks(self, start_line=0, end_line=None):
        if end_line is None or end_line > len(self.__chunks):
            end_line = len(self.__chunks)
        if start_line >= len(self.__chunks) or end_line <= start_line:
            return

        prev_chunk = None
        for i in xrange(start_line, end_line):
            chunk = self.__chunks[i]
            if chunk != prev_chunk:
                yield chunk
            prev_chunk = chunk

    def __freeze_changes(self):
        self.__freeze_changes_count += 1

    def __thaw_changes(self):
        self.__freeze_changes_count -= 1
        if self.__freeze_changes_count == 0:
            self.rescan()
            self.__emit_chunk_changes()

    def __emit_chunk_changes(self):
        deleted_chunks = self.__deleted_chunks
        self.__deleted_chunks = set()

        changed_chunks = self.__changed_chunks
        self.__changed_chunks = set()

        for chunk in deleted_chunks:
            self.emit('chunk-deleted', chunk)

        for chunk in sorted(changed_chunks, lambda a, b: cmp(a.start,b.start)):
            if chunk.newly_inserted:
                chunk.newly_inserted = False
                chunk.changes.clear()
                chunk.status_changed = False
                self.emit('chunk-inserted', chunk)
            elif not chunk.changes.empty():
                changed_lines = range(chunk.changes.start, chunk.changes.end)
                chunk.changes.clear()
                chunk.status_changed = False
                self.emit('chunk-changed', chunk, changed_lines)
            if isinstance(chunk, StatementChunk) and chunk.status_changed:
                chunk.status_changed = False
                self.emit('chunk-status-changed', chunk)
            if isinstance(chunk, StatementChunk) and chunk.results_changed:
                chunk.results_changed = False
                self.emit('chunk-results-changed', chunk)

    def __chunk_changed(self, chunk):
        self.__changed_chunks.add(chunk)

    def __mark_rest_for_execute(self, start_line):
        if self.state != NotebookFile.NEEDS_EXECUTE:
            self.__set_state(NotebookFile.NEEDS_EXECUTE)

        # Mark all statements starting from start_line as needing execution.
        # We do this immediately when we change or delete a previous
        # StatementChunk. The alternative would be to do it when we
        # __thaw_changes(), which would conceivably be more efficient, but
        # it's hard to see how to handle deleted chunks in that case.
        for chunk in self.iterate_chunks(start_line):
            if isinstance(chunk, StatementChunk):
                if chunk.mark_for_execute():
                    self.__chunk_changed(chunk)
                else:
                    # Everything after the first chunk that was previously
                    # marked for execution must also have been marked for
                    # execution, so we can stop
                    break

    def __mark_changed_statement(self, chunk):
        self.__chunk_changed(chunk)
        self.__mark_rest_for_execute(chunk.end)

    def __remove_chunk(self, chunk):
        try:
            self.__changed_chunks.remove(chunk)
        except KeyError:
            pass
        if not chunk.newly_inserted:
            self.__deleted_chunks.add(chunk)
        if isinstance(chunk, StatementChunk):
            self.__mark_rest_for_execute(chunk.end)

    def __adjust_or_create_chunk(self, start, end, line_class):
        if line_class == BLANK:
            klass = BlankChunk
        elif line_class == COMMENT:
            klass = CommentChunk
        else:
            klass = StatementChunk

        # Look for an existing chunk of the right type
        chunk = None
        for i in xrange(start, end):
            if isinstance(self.__chunks[i], klass):
                chunk = self.__chunks[i]
                break

        if chunk is not None:
            if chunk.end > end:
                # An old statement can only be turned into *one* new statement; once
                # we've used the chunk, we can't use it again
                self.__chunks[end:chunk.end] = (None for i in xrange(end, chunk.end))
        else:
            chunk = klass()

        chunk.set_range(start, end)
        for c in self.iterate_chunks(start, end):
            assert c.start >= start

            if c == chunk:
                pass
            elif c.end <= end:
                self.__remove_chunk(c)
            else:
                c.set_range(end, c.end)

        self.__chunks[start:end] = (chunk for i in xrange(start, end))

        return chunk

    def __assign_lines(self, chunk_start, lines, statement_end):
        if statement_end > chunk_start:
            chunk_lines = lines[0:statement_end - chunk_start]
            chunk = self.__adjust_or_create_chunk(chunk_start, statement_end, STATEMENT_START)
            chunk.set_lines(chunk_lines)

            if not chunk.changes.empty():
                self.__mark_changed_statement(chunk)

        start = statement_end
        prev_class = CONTINUATION # Doesn't matter, not blank/continuation
        for i in xrange(statement_end, chunk_start + len(lines)):
            line_class = calc_line_class(self.__lines[i])
            if line_class != prev_class and i > start:
                chunk = self.__adjust_or_create_chunk(start, i, prev_class)
                if not chunk.changes.empty():
                    self.__chunk_changed(chunk)
                start = i
            prev_class = line_class

        if chunk_start + len(lines) > start:
            chunk = self.__adjust_or_create_chunk(start, chunk_start + len(lines), prev_class)
            if not chunk.changes.empty():
                self.__chunk_changed(chunk)

    def rescan(self):
        """Update the division of the worksheet into chunks based on the current text.

        As the buffer is edited, the division of the buffer into chunks is updated
        blindly without attention to the details of the new text. Normally, we will
        rescan and figure out the real chunks at the end of a user operation, however
        it is occasionally useful to do this early, for example, if we want to use
        the tokenized representation of a statement for the second part of a user
        operation.

        """

        _debug("  Changed %s,%s (%s), scan_adjacent=%d", self.__changes.start, self.__changes.end, self.__changes.delta, self.__scan_adjacent)

        if self.__changes.empty():
            return

        if self.__scan_adjacent:
            rescan_start = self.__changes.start
            rescan_end = self.__changes.end

            while rescan_start > 0:
                rescan_start -= 1
                chunk = self.__chunks[rescan_start]
                if isinstance(chunk, StatementChunk):
                    rescan_start = chunk.start
                    break

            while rescan_end < len(self.__lines):
                chunk = self.__chunks[rescan_end]
                # The check for continuation line is needed because the first statement
                # in a buffer can start with a continuation line
                if isinstance(chunk, StatementChunk) and \
                        chunk.start == rescan_end and \
                        not CONTINUATION_RE.match(self.__lines[chunk.start]):
                    break
                rescan_end = chunk.end
        else:
            rescan_start = self.__changes.start
            rescan_end = self.__changes.end

        self.__changes.clear()
        self.__scan_adjacent = False

        if self.__chunks[rescan_start] is not None:
            rescan_start = self.__chunks[rescan_start].start;
        if self.__chunks[rescan_end - 1] is not None:
            rescan_end = self.__chunks[rescan_end - 1].end;

        _debug("  Rescanning lines %s-%s", rescan_start, rescan_end)

        chunk_start = rescan_start
        statement_end = rescan_start
        chunk_lines = []

        seen_start = False
        for line in xrange(rescan_start, rescan_end):
            line_text = self.__lines[line]

            line_class = calc_line_class(line_text)
            if line_class == BLANK:
                chunk_lines.append(line_text)
            elif line_class == COMMENT:
                chunk_lines.append(line_text)
            elif line_class == CONTINUATION and seen_start:
                chunk_lines.append(line_text)
                statement_end = line + 1
            else:
                seen_start = True
                if len(chunk_lines) > 0:
                    self.__assign_lines(chunk_start, chunk_lines, statement_end)
                chunk_start = line
                statement_end = line + 1
                chunk_lines = [line_text]

        self.__assign_lines(chunk_start, chunk_lines, statement_end)

    def __set_line(self, line, text):
        if self.__lines[line] is not None:
            old_class = calc_line_class(self.__lines[line])
        else:
            old_class = None
        self.__lines[line] = text
        if old_class != calc_line_class(text):
            self.__scan_adjacent = True
        self.__changes.change(line, line + 1)

    def begin_user_action(self):
        self.__user_action_count += 1
        self.__undo_stack.begin_user_action()
        self.__freeze_changes()

    def end_user_action(self):
        self.__user_action_count -= 1
        self.__thaw_changes()
        self.__undo_stack.end_user_action()

    def in_user_action(self):
        return self.__user_action_count > 0

    def __insert_lines(self, line, count, chunk):
        # Insert an integral number of lines into the given chunk at the given position
        # fixing up the chunk and the __chunks[]/__lines[] arrays

        self.__chunks[line:line] = (chunk for i in xrange(count))
        self.__lines[line:line] = (None for i in xrange(count))
        chunk.insert_lines(line, count)

        # Fix up the subsequent chunks
        for c in self.iterate_chunks(chunk.end):
            c.start += count
            c.end += count

        self.__changes.insert(line, count)
        self.__scan_adjacent = True
        self.__chunk_changed(chunk)
        self.emit('lines-inserted', line, line + count)

    def insert(self, line, offset, text):
        _debug("Inserting %r at %s,%s", text, line, offset)
        if len(text) == 0:
            return

        if self.state == NotebookFile.EXECUTING:
            return

        self.__freeze_changes()

        self.emit('text-inserted', line, offset, text)

        count = 0
        ends_with_new_line = False
        for m in NEW_LINE_RE.finditer(text):
            count += 1
            ends_with_new_line = m.end() == len(text)

        chunk = self.__chunks[line]
        left = self.__lines[line][0:offset]
        right = self.__lines[line][offset:]

        if count == 0:
            # Change within a single line
            self.__set_line(line, left + text + right)
            chunk.change_line(line)
            end_line = line
            end_offset = offset + len(text)
        else:
            if offset == 0 and ends_with_new_line:
                # This is a pure insertion of an integral number of lines

                # At a chunk boundary, extend the chunk before, not the chunk after
                if line > 0 and chunk.start == line:
                    chunk = self.__chunks[line - 1]

                self.__insert_lines(line, count, chunk)
            else:
                if offset == 0:
                    self.__insert_lines(line, count, chunk)
                    chunk.change_line(line + count)
                else:
                    self.__insert_lines(line + 1, count, chunk)
                    chunk.change_line(line)

            # Now set the new text into the lines array
            iter = NEW_LINE_RE.finditer(text)
            i = line

            m = iter.next()
            self.__set_line(line, left + text[0:m.start()])
            last = m.end()
            i += 1

            while True:
                try:
                    m = iter.next()
                except StopIteration:
                    break

                self.__set_line(i, text[last:m.start()])
                last = m.end()
                i += 1

            if not (offset == 0 and ends_with_new_line):
                self.__set_line(i, text[last:] + right)

            end_line = i
            end_offset = len(text) - last

        self.__thaw_changes()
        self.__undo_stack.append_op(InsertOp((line, offset), (end_line, end_offset), text))

        if self.__user_action_count > 0 and not self.code_modified:
            self.code_modified = True

    def __delete_lines(self, start_line, end_line):
        # Delete an integral number of lines, fixing up the affected chunks
        # and the __chunks[]/__lines[] arrays

        if end_line == start_line: # No lines deleted
            return

        for chunk in self.iterate_chunks(start_line):
            if chunk.start >= end_line:
                chunk.start -= (end_line - start_line)
                chunk.end -= (end_line - start_line)
            elif chunk.start >= start_line:
                if chunk.end <= end_line:
                    self.__remove_chunk(chunk)
                else:
                    chunk.delete_lines(chunk.start, end_line)
                    self.__chunk_changed(chunk)
                    chunk.end -= chunk.start - start_line
                    chunk.start = start_line
            else:
                chunk.delete_lines(start_line, min(chunk.end, end_line))
                self.__chunk_changed(chunk)

        self.__lines[start_line:end_line] = ()
        self.__chunks[start_line:end_line] = ()

        self.__changes.delete_range(start_line, end_line)
        self.__scan_adjacent = True
        self.emit('lines-deleted', start_line, end_line)

    def delete_range(self, start_line, start_offset, end_line, end_offset):
        _debug("Deleting from %s,%s to %s,%s", start_line, start_offset, end_line, end_offset)

        if self.state == NotebookFile.EXECUTING:
            return

        if start_line == end_line and start_offset == end_offset:
            return

        self.__freeze_changes()

        start_line, start_offset, end_line, end_offset = order_positions(start_line, start_offset, end_line, end_offset)

        deleted_text = self.get_text(start_line, start_offset, end_line, end_offset)

        self.emit('text-deleted', start_line, start_offset, end_line, end_offset)

        if start_offset == 0 and end_offset == 0:
            # Deleting some whole number of lines
            self.__delete_lines(start_line, end_line)
        else:
            left = self.__lines[start_line][0:start_offset]
            right = self.__lines[end_line][end_offset:]

            if start_offset == 0:
                self.__delete_lines(start_line, end_line)
            else:
                self.__delete_lines(start_line + 1, end_line + 1)

            self.__set_line(start_line, left + right)
            chunk = self.__chunks[start_line]
            chunk.change_line(start_line)
            self.__chunk_changed(chunk)

        self.__thaw_changes()
        self.__undo_stack.append_op(DeleteOp((start_line, start_offset), (end_line, end_offset), deleted_text))

        if self.__user_action_count > 0 and not self.code_modified:
            self.code_modified = True

    def place_cursor(self, line, offset):
        _debug("Place cursor at %s,%s", line, offset)
        self.emit('place-cursor', line, offset)

    def undo(self):
        self.__undo_stack.undo()

    def redo(self):
        self.__undo_stack.redo()

    def module_changed(self, module_name):
        """Mark statements for execution after a change to the given module"""

        for chunk in self.iterate_chunks():
            if not isinstance(chunk, StatementChunk):
                continue
            if chunk.statement is None:
                continue

            imports = chunk.statement.imports
            if imports is None:
                continue

            for module, _ in imports:
                if module == module_name:
                    self.__mark_rest_for_execute(chunk.start)

    def calculate(self, wait=False):
        _debug("Calculating")

        self.__freeze_changes()

        parent = None
        have_error = False

        executor = None

        for chunk in self.iterate_chunks():
            if isinstance(chunk, StatementChunk):
                changed = False

                if chunk.needs_compile or chunk.needs_execute:
                    if not executor:
                        executor = ThreadExecutor(parent)

                if executor:
                    statement = chunk.get_statement(self)
                    executor.add_statement(statement)

                parent = chunk.statement

        if executor:
            if wait:
                loop = gobject.MainLoop()

            def on_statement_execution_state_changed(executor, statement):
                if (statement.state == Statement.COMPILE_ERROR or
                    statement.state == Statement.EXECUTE_ERROR or
                    statement.state == Statement.INTERRUPTED):
                    self.__executor_error = True

                statement.chunk.update_statement()

                if self.__freeze_changes_count == 0:
                    self.__freeze_changes()
                    self.__chunk_changed(statement.chunk)
                    self.__thaw_changes()
                else:
                    self.__chunk_changed(statement.chunk)

            def on_complete(executor):
                self.__executor = None
                self.__set_state(NotebookFile.ERROR if self.__executor_error else NotebookFile.EXECUTE_SUCCESS)
                if wait:
                    loop.quit()

            self.__executor = executor
            self.__executor_error = False
            self.__set_state(NotebookFile.EXECUTING)
            executor.connect('statement-executing', on_statement_execution_state_changed)
            executor.connect('statement-complete', on_statement_execution_state_changed)
            executor.connect('complete', on_complete)

            if executor.compile():
                executor.execute()
                if wait:
                    loop.run()
        else:
            # Nothing to execute, we could have been in a non-success state if statements were deleted
            # at the end of the file.
            self.__set_state(NotebookFile.EXECUTE_SUCCESS)

        self.__thaw_changes()

    def interrupt(self):
        if self.state == NotebookFile.EXECUTING:
            self.__executor.interrupt()

    def __get_last_scope(self, chunk):
        # Get the last result scope we have that precedes the specified chunk

        scope = None
        line = chunk.start - 1
        while line >= 0:
            previous_chunk = self.__chunks[line]

            # We intentionally don't check "needs_execute" ... if there is a result scope,
            # it's fair game for completion/help, even if it's old
            if isinstance(previous_chunk, StatementChunk) and previous_chunk.statement is not None and previous_chunk.statement.result_scope is not None:
                return previous_chunk.statement.result_scope
                break

            line = previous_chunk.start - 1

        return self.global_scope

    def find_completions(self, line, offset, min_length=0):
        """Returns a list of possible completions at the given position.

        Each element in the returned list is a tuple of (display_form,
        text_to_insert, object_completed_to)' where
        object_completed_to can be used to determine the type of the
        completion or get docs about it.

        @param min_length if supplied, the minimum length to require for an isolated
           name before we complete against the scope. This is useful if we are suggesting
           completions without the user explicitly requesting it.

        """

        chunk = self.__chunks[line]
        if not isinstance(chunk, StatementChunk) and not isinstance(chunk, BlankChunk):
            return []

        scope = self.__get_last_scope(chunk)

        if isinstance(chunk, StatementChunk):
            return chunk.tokenized.find_completions(line - chunk.start,
                                                    offset,
                                                    scope,
                                                    min_length=min_length)
        else:
            # A BlankChunk Create a dummy TokenizedStatement to get the completions
            # appropriate for the start of a line
            ts = TokenizedStatement()
            ts.set_lines([''])
            return ts.find_completions(0, 0, scope, min_length=min_length)

    def get_object_at_location(self, line, offset, include_adjacent=False):
        """Find the object at a particular location within the worksheet

        @param include_adjacent: if False, then location identifies a character in the worksheet. If True,
           then location identifies a position between characters, and symbols before or after that
           position are included.

        @returns: a tuple of (object, start_line, start_offset, end_line, end_offset) or (None, None, None, None, None)

        """

        chunk = self.__chunks[line]
        if not isinstance(chunk, StatementChunk):
            return None, None, None, None, None

        if chunk.statement is not None and chunk.statement.result_scope is not None:
            result_scope = chunk.statement.result_scope
        else:
            result_scope = None

        obj, start_line, start_index, end_line, end_index = \
            chunk.tokenized.get_object_at_location(line - chunk.start, offset,
                                                   self.__get_last_scope(chunk),
                                                   result_scope, include_adjacent)

        if obj is None:
            return None, None, None, None, None

        start_line += chunk.start
        end_line += chunk.start

        return obj, start_line, start_index, end_line, end_index

    def __do_clear(self):
        self.delete_range(0, 0, len(self.__lines) - 1, len(self.__lines[len(self.__lines) - 1]));

    def clear(self):
        self.__do_clear()
        self.__set_filename_and_modified(None, False)

        # XXX: This prevents redoing New, would that "just work"?
        self.__undo_stack.clear()

    def get_text(self, start_line=0, start_offset=0, end_line=-1, end_offset=-1):
        if start_line < 0:
            start_line = len(self.__lines) -1
        if end_line < 0:
            end_line = len(self.__lines) -1
        if start_offset < 0:
            start_offset = len(self.__lines[start_line])
        if end_offset < 0:
            end_offset = len(self.__lines[end_line])

        start_line, start_offset, end_line, end_offset = order_positions(start_line, start_offset, end_line, end_offset)

        if start_line == end_line:
            return self.__lines[start_line][start_offset:end_offset]

        si = StringIO()

        line = start_line
        si.write(self.__lines[line][start_offset:])
        line += 1

        while line < end_line:
            si.write("\n")
            si.write(self.__lines[line][start_offset:])
            line += 1

        si.write("\n")
        si.write(self.__lines[line][:end_offset])

        return si.getvalue()

    def get_doctests(self, start_line, end_line):
        si = StringIO()

        first = True
        for chunk in self.iterate_chunks(start_line, end_line + 1):
            for i in xrange(chunk.start, chunk.end):
                line_text = self.__lines[i]
                if isinstance(chunk, StatementChunk):
                    if i != chunk.start:
                        si.write("... ")
                    else:
                        si.write(">>> ")
                si.write(line_text)
                # Don't turn a trailing newline into two
                if i != len(self.__lines) - 1 or len(line_text) > 0:
                    si.write("\n")

            if isinstance(chunk, StatementChunk) and chunk.results is not None:
                for result in chunk.results:
                    if isinstance(result, basestring):
                        si.write(result)
                        si.write("\n")

        return si.getvalue()

    def get_line_count(self):
        return len(self.__lines)

    def get_chunk(self, line):
        return self.__chunks[line]

    def get_line(self, line):
        return self.__lines[line]

    def __set_state(self, new_state):
        if self.edit_only:
            return
        self.state = new_state
        if self.__file:
            self.__file.state = new_state

    def __set_filename(self, filename):
        if filename == self.__filename:
            return

        if self.__file:
            self.__file.worksheet = None
            self.__file.modified = False
            self.__file.active = False

        self.__filename = filename
        if filename:
            self.__file = self.notebook.file_for_absolute_path(self.__filename)
            if self.__file:
                self.__file.worksheet = self
                self.__file.active = True
                self.__file.modified = self.__code_modified
        else:
            self.__file = None

    def __get_filename(self):
        return self.__filename

    filename = gobject.property(getter=__get_filename, setter=__set_filename, type=str, default=None)

    @gobject.property
    def file(self):
        return self.__file

    def __set_code_modified(self, code_modified):
        if code_modified == self.__code_modified:
            return

        self.__code_modified = code_modified
        if self.__file:
            self.__file.modified = code_modified

    def __get_code_modified(self):
        return self.__code_modified

    code_modified = gobject.property(getter=__get_code_modified, setter=__set_code_modified, type=bool, default=False)
    state = gobject.property(type=int, default=NotebookFile.EXECUTE_SUCCESS)

    def __set_filename_and_modified(self, filename, modified):
        self.freeze_notify()
        self.filename = filename
        self.code_modified = modified
        self.thaw_notify()

    def load(self, filename, escape=False):
        """Load a file from disk into the worksheet. Can raise IOError if the
        file cannot be read, and reunicode.ConversionError if the file contains
        invalid characters. (reunicode.ConversionError will not be raised if
        escape is True)

        @param filename the file to load
        @param escape if true, invalid byte and character sequences in the input
           will be converted into \\x<nn> and \\u<nnnn> escape sequences.

        """
        f = open(filename)
        text = f.read()
        f.close()

        self.__do_clear()
        self.insert(0, 0, reunicode.decode(text, escape=escape))
        # A bit of a hack - we assume that if escape was passed we *did* escape.
        # this is the way that things work currently - first the GUI loads with
        # escape=False, and if that fails, prompts the user and loads with escape=True
        self.__set_filename_and_modified(filename, escape)
        self.__undo_stack.clear()

    def save(self, filename=None):
        if filename is None:
            if self.__filename is None:
                raise ValueError("No current or specified filename")

            filename = self.__filename

        if not self.code_modified and filename == self.__filename:
            return

        filename_changed = filename != self.__filename

        tmpname = filename + ".tmp"

        # We use binary mode, since we don't want to munge line endings to the system default
        # on a load-save cycle
        f = open(tmpname, "wb")

        success = False
        try:
            first = True
            for line in self.__lines:
                if not first:
                    f.write("\n")
                first = False
                f.write(line.encode("utf8"))

            f.close()
            # Windows can't save over an existing filename; we might want to check os.name to
            # see if we have to do this, but it's unlikely that the unlink will succeed and
            # the rename fail, so I think it's 'atomic' enough this way.
            if os.path.exists(filename):
                os.unlink(filename)
            os.rename(tmpname, filename)
            success = True

            # Need to refresh the notebook before saving so that we find the NotebookFile
            # properly in __set_filename_and_modified
            if filename_changed:
                self.notebook.refresh()
            self.__set_filename_and_modified(filename, False)
            if self.notebook.info:
                self.notebook.info.update_last_modified()
        finally:
            if not success:
                f.close()
                try:
                    os.remove(tmpname)
                except:
                    pass

    def close(self):
        if self.__file:
            self.__file.worksheet = None
            self.__file.modified = False
            self.__file.state = NotebookFile.NONE
            self.__file.active = False

        self.notebook._remove_worksheet(self)
コード例 #3
0
class Worksheet(object):
    def __init__(self, notebook, edit_only=False):
        # Chunk changed is emitted when the text or tokenization of a chunk
        # changes. Note that "changes" here specifically includes being
        # replaced by identical text, so if I have the two chunks
        #
        #  if
        #  if
        #
        # And I delete the from the first 'i' to the second f, the first
        # chunk is considered to change, even though it's text remains 'if'.
        # This is because text in a buffering that is shadowing us may
        # be tagged with fonts/styles.
        #
        self.sig_chunk_inserted = signals.Signal()
        self.sig_chunk_changed = signals.Signal()
        self.sig_chunk_deleted = signals.Signal()
        self.sig_chunk_status_changed = signals.Signal()
        self.sig_chunk_results_changed = signals.Signal()

        # text-* are emitted before we fix up our internal state, so what can be done
        # in them are limited. They are meant for keeping a UI in sync with the internal
        # state.
        self.sig_text_inserted = signals.Signal()
        self.sig_text_deleted = signals.Signal()

        self.sig_lines_inserted = signals.Signal()
        self.sig_lines_deleted = signals.Signal()

        # This is only for the convenience of the undo stack; otherwise we ignore cursor position
        self.sig_place_cursor = signals.Signal()

        self.notebook = notebook
        self.edit_only = edit_only

        self.__file = None
        self.sig_file = signals.Signal()

        self.__filename = None
        self.sig_filename_changed = signals.Signal()

        self.__code_modified = False
        self.sig_code_modified = signals.Signal()

        self.__state = NotebookFile.EXECUTE_SUCCESS
        self.sig_state = signals.Signal()

        self.global_scope = {}
        notebook.setup_globals(self.global_scope)
        exec _DEFINE_GLOBALS in self.global_scope

        self.__lines = [""]
        self.__chunks = [BlankChunk(0, 1)]

        # There's quite a bit of complexity knowing when a change to lines changes
        # adjacent chunks. We use a simple and slightly inefficient algorithm for this
        # and just scan everything that might have changed. But we don't want typing
        # within a line to cause an unlimited rescan, so we keep track if the only
        # changes we've made are inserting/deleting within a line without changing
        # that lines class
        self.__changes = ChangeRange()
        self.__scan_adjacent = False

        self.__changed_chunks = set()
        self.__deleted_chunks = set()
        self.__freeze_changes_count = 0
        self.__user_action_count = 0

        self.__undo_stack = UndoStack(self)
        self.__executor = None

        notebook._add_worksheet(self)

    def destroy(self):
        if self.__executor:
            # Interruption is handled at a higher level
            self.__executor.destroy()

        if self.__file:
            self.__file.worksheet = None
            self.__file.modified = False
            self.__file.state = NotebookFile.NONE
            self.__file.active = False

        self.notebook._remove_worksheet(self)

        self.sig_chunk_inserted.disconnectAll()
        self.sig_chunk_changed.disconnectAll()
        self.sig_chunk_deleted.disconnectAll()
        self.sig_chunk_status_changed.disconnectAll()
        self.sig_chunk_results_changed.disconnectAll()

        self.sig_text_inserted.disconnectAll()
        self.sig_text_deleted.disconnectAll()
        self.sig_lines_inserted.disconnectAll()
        self.sig_lines_deleted.disconnectAll()

        self.sig_place_cursor.disconnectAll()

        self.sig_file.disconnectAll()
        self.sig_filename_changed.disconnectAll()
        self.sig_code_modified.disconnectAll()
        self.sig_state.disconnectAll()
        pass

    #######################################################

    def do_import(self, name, globals, locals, fromlist, level):
        __import__(self, name, globals, locals, fromlist, level)

    def iterate_chunks(self, start_line=0, end_line=None):
        if end_line is None or end_line > len(self.__chunks):
            end_line = len(self.__chunks)
        if start_line >= len(self.__chunks) or end_line <= start_line:
            return

        prev_chunk = None
        for i in xrange(start_line, end_line):
            chunk = self.__chunks[i]
            if chunk != prev_chunk:
                yield chunk
            prev_chunk = chunk

    def __freeze_changes(self):
        self.__freeze_changes_count += 1

    def __thaw_changes(self):
        self.__freeze_changes_count -= 1
        if self.__freeze_changes_count == 0:
            self.rescan()
            self.__emit_chunk_changes()

    def __emit_chunk_changes(self):
        deleted_chunks = self.__deleted_chunks
        self.__deleted_chunks = set()

        changed_chunks = self.__changed_chunks
        self.__changed_chunks = set()

        for chunk in deleted_chunks:
            self.sig_chunk_deleted(self, chunk)

        for chunk in sorted(changed_chunks,
                            lambda a, b: cmp(a.start, b.start)):
            if chunk.newly_inserted:
                chunk.newly_inserted = False
                chunk.changes.clear()
                chunk.status_changed = False
                self.sig_chunk_inserted(self, chunk)
            elif not chunk.changes.empty():
                changed_lines = range(chunk.changes.start, chunk.changes.end)
                chunk.changes.clear()
                chunk.status_changed = False
                self.sig_chunk_changed(self, chunk, changed_lines)
            if isinstance(chunk, StatementChunk) and chunk.status_changed:
                chunk.status_changed = False
                self.sig_chunk_status_changed(self, chunk)
            if isinstance(chunk, StatementChunk) and chunk.results_changed:
                chunk.results_changed = False
                self.sig_chunk_results_changed(self, chunk)

    def __chunk_changed(self, chunk):
        self.__changed_chunks.add(chunk)

    def __mark_rest_for_execute(self, start_line):
        if self.state != NotebookFile.NEEDS_EXECUTE:
            self.__set_state(NotebookFile.NEEDS_EXECUTE)

        # Mark all statements starting from start_line as needing execution.
        # We do this immediately when we change or delete a previous
        # StatementChunk. The alternative would be to do it when we
        # __thaw_changes(), which would conceivably be more efficient, but
        # it's hard to see how to handle deleted chunks in that case.
        for chunk in self.iterate_chunks(start_line):
            if isinstance(chunk, StatementChunk):
                if chunk.mark_for_execute():
                    self.__chunk_changed(chunk)
                else:
                    # Everything after the first chunk that was previously
                    # marked for execution must also have been marked for
                    # execution, so we can stop
                    break

    def __mark_changed_statement(self, chunk):
        self.__chunk_changed(chunk)
        self.__mark_rest_for_execute(chunk.end)

    def __remove_chunk(self, chunk):
        try:
            self.__changed_chunks.remove(chunk)
        except KeyError:
            pass
        if not chunk.newly_inserted:
            self.__deleted_chunks.add(chunk)
        if isinstance(chunk, StatementChunk):
            self.__mark_rest_for_execute(chunk.end)

    def __adjust_or_create_chunk(self, start, end, line_class):
        if line_class == BLANK:
            klass = BlankChunk
        elif line_class == COMMENT:
            klass = CommentChunk
        else:
            klass = StatementChunk

        # Look for an existing chunk of the right type
        chunk = None
        for i in xrange(start, end):
            if isinstance(self.__chunks[i], klass):
                chunk = self.__chunks[i]
                break

        if chunk is not None:
            if chunk.end > end:
                # An old statement can only be turned into *one* new statement; once
                # we've used the chunk, we can't use it again
                self.__chunks[end:chunk.end] = (
                    None for i in xrange(end, chunk.end))
        else:
            chunk = klass()

        chunk.set_range(start, end)
        for c in self.iterate_chunks(start, end):
            assert c.start >= start

            if c == chunk:
                pass
            elif c.end <= end:
                self.__remove_chunk(c)
            else:
                c.set_range(end, c.end)

        self.__chunks[start:end] = (chunk for i in xrange(start, end))

        return chunk

    def __assign_lines(self, chunk_start, lines, statement_end):
        if statement_end > chunk_start:
            chunk_lines = lines[0:statement_end - chunk_start]
            chunk = self.__adjust_or_create_chunk(chunk_start, statement_end,
                                                  STATEMENT_START)
            chunk.set_lines(chunk_lines)

            if not chunk.changes.empty():
                self.__mark_changed_statement(chunk)

        start = statement_end
        prev_class = CONTINUATION  # Doesn't matter, not blank/continuation
        for i in xrange(statement_end, chunk_start + len(lines)):
            line_class = calc_line_class(self.__lines[i])
            if line_class != prev_class and i > start:
                chunk = self.__adjust_or_create_chunk(start, i, prev_class)
                if not chunk.changes.empty():
                    self.__chunk_changed(chunk)
                start = i
            prev_class = line_class

        if chunk_start + len(lines) > start:
            chunk = self.__adjust_or_create_chunk(start,
                                                  chunk_start + len(lines),
                                                  prev_class)
            if not chunk.changes.empty():
                self.__chunk_changed(chunk)

    def rescan(self):
        """Update the division of the worksheet into chunks based on the current text.

        As the buffer is edited, the division of the buffer into chunks is updated
        blindly without attention to the details of the new text. Normally, we will
        rescan and figure out the real chunks at the end of a user operation, however
        it is occasionally useful to do this early, for example, if we want to use
        the tokenized representation of a statement for the second part of a user
        operation.

        """

        _debug("  Changed %s,%s (%s), scan_adjacent=%d", self.__changes.start,
               self.__changes.end, self.__changes.delta, self.__scan_adjacent)

        if self.__changes.empty():
            return

        if self.__scan_adjacent:
            rescan_start = self.__changes.start
            rescan_end = self.__changes.end

            while rescan_start > 0:
                rescan_start -= 1
                chunk = self.__chunks[rescan_start]
                if isinstance(chunk, StatementChunk):
                    rescan_start = chunk.start
                    break

            # See if the last (non-blank, non-comment) line of the chunk
            # we're rescanning is a decorator
            prev_decorator = False
            line = rescan_end
            while line > 0:
                line -= 1
                line_class = calc_line_class(self.__lines[line])
                if line_class in (STATEMENT_START, CONTINUATION):
                    break
                elif line_class == DECORATOR:
                    prev_decorator = True
                    break

            while rescan_end < len(self.__lines):
                chunk = self.__chunks[rescan_end]
                # The check for continuation line is needed because the first statement
                # in a buffer can start with a continuation line
                if isinstance(chunk, StatementChunk) and \
                        chunk.start == rescan_end and \
                        not CONTINUATION_RE.match(self.__lines[chunk.start]) and \
                        not prev_decorator:
                    break
                # A StatementChunk cannot end with a decorator.  Thus, the next chunk
                # cannot be following a decorator.
                if isinstance(chunk, StatementChunk):
                    prev_decorator = False
                rescan_end = chunk.end
        else:
            rescan_start = self.__changes.start
            rescan_end = self.__changes.end

        self.__changes.clear()
        self.__scan_adjacent = False

        if rescan_start == rescan_end:
            return

        if self.__chunks[rescan_start] is not None:
            rescan_start = self.__chunks[rescan_start].start
        if self.__chunks[rescan_end - 1] is not None:
            rescan_end = self.__chunks[rescan_end - 1].end

        _debug("  Rescanning lines %s-%s", rescan_start, rescan_end)

        chunk_start = rescan_start
        statement_end = rescan_start
        chunk_lines = []

        seen_start = False
        prev_decorator = False
        for line in xrange(rescan_start, rescan_end):
            line_text = self.__lines[line]

            line_class = calc_line_class(line_text)
            if line_class == BLANK:
                chunk_lines.append(line_text)
            elif line_class == COMMENT:
                chunk_lines.append(line_text)
            elif (line_class == CONTINUATION and seen_start) or prev_decorator:
                chunk_lines.append(line_text)
                statement_end = line + 1
                prev_decorator = (line_class == DECORATOR)
            else:
                seen_start = True
                if len(chunk_lines) > 0:
                    self.__assign_lines(chunk_start, chunk_lines,
                                        statement_end)
                chunk_start = line
                statement_end = line + 1
                chunk_lines = [line_text]
                prev_decorator = (line_class == DECORATOR)

        self.__assign_lines(chunk_start, chunk_lines, statement_end)

    def __set_line(self, line, text):
        if self.__lines[line] is not None:
            old_class = calc_line_class(self.__lines[line])
        else:
            old_class = None
        self.__lines[line] = text
        if old_class != calc_line_class(text):
            self.__scan_adjacent = True
        self.__changes.change(line, line + 1)

    def begin_user_action(self):
        self.__user_action_count += 1
        self.__undo_stack.begin_user_action()
        self.__freeze_changes()

    def end_user_action(self):
        self.__user_action_count -= 1
        self.__thaw_changes()
        self.__undo_stack.end_user_action()

    def in_user_action(self):
        return self.__user_action_count > 0

    def __insert_lines(self, line, count, chunk):
        # Insert an integral number of lines into the given chunk at the given position
        # fixing up the chunk and the __chunks[]/__lines[] arrays

        self.__chunks[line:line] = (chunk for i in xrange(count))
        self.__lines[line:line] = (None for i in xrange(count))
        chunk.insert_lines(line, count)

        # Fix up the subsequent chunks
        for c in self.iterate_chunks(chunk.end):
            c.start += count
            c.end += count

        self.__changes.insert(line, count)
        self.__scan_adjacent = True
        self.__chunk_changed(chunk)
        self.sig_lines_inserted(self, line, line + count)

    def insert(self, line, offset, text):
        _debug("Inserting %r at %s,%s", text, line, offset)
        if len(text) == 0:
            return

        if self.state == NotebookFile.EXECUTING:
            return

        self.__freeze_changes()

        self.sig_text_inserted(self, line, offset, text)

        count = 0
        ends_with_new_line = False
        for m in NEW_LINE_RE.finditer(text):
            count += 1
            ends_with_new_line = m.end() == len(text)

        chunk = self.__chunks[line]
        left = self.__lines[line][0:offset]
        right = self.__lines[line][offset:]

        if count == 0:
            # Change within a single line
            self.__set_line(line, left + text + right)
            chunk.change_line(line)
            end_line = line
            end_offset = offset + len(text)
        else:
            if offset == 0 and ends_with_new_line:
                # This is a pure insertion of an integral number of lines

                # At a chunk boundary, extend the chunk before, not the chunk after
                if line > 0 and chunk.start == line:
                    chunk = self.__chunks[line - 1]

                self.__insert_lines(line, count, chunk)
            else:
                if offset == 0:
                    self.__insert_lines(line, count, chunk)
                    chunk.change_line(line + count)
                else:
                    self.__insert_lines(line + 1, count, chunk)
                    chunk.change_line(line)

            # Now set the new text into the lines array
            iter = NEW_LINE_RE.finditer(text)
            i = line

            m = iter.next()
            self.__set_line(line, left + text[0:m.start()])
            last = m.end()
            i += 1

            while True:
                try:
                    m = iter.next()
                except StopIteration:
                    break

                self.__set_line(i, text[last:m.start()])
                last = m.end()
                i += 1

            if not (offset == 0 and ends_with_new_line):
                self.__set_line(i, text[last:] + right)

            end_line = i
            end_offset = len(text) - last

        self.__thaw_changes()
        self.__undo_stack.append_op(
            InsertOp((line, offset), (end_line, end_offset), text))

        if self.__user_action_count > 0 and not self.code_modified:
            self.code_modified = True

    def __delete_lines(self, start_line, end_line):
        # Delete an integral number of lines, fixing up the affected chunks
        # and the __chunks[]/__lines[] arrays

        if end_line == start_line:  # No lines deleted
            return

        for chunk in self.iterate_chunks(start_line):
            if chunk.start >= end_line:
                chunk.start -= (end_line - start_line)
                chunk.end -= (end_line - start_line)
            elif chunk.start >= start_line:
                if chunk.end <= end_line:
                    self.__remove_chunk(chunk)
                else:
                    chunk.delete_lines(chunk.start, end_line)
                    self.__chunk_changed(chunk)
                    chunk.end -= chunk.start - start_line
                    chunk.start = start_line
            else:
                chunk.delete_lines(start_line, min(chunk.end, end_line))
                self.__chunk_changed(chunk)

        self.__lines[start_line:end_line] = ()
        self.__chunks[start_line:end_line] = ()

        self.__changes.delete_range(start_line, end_line)
        self.__scan_adjacent = True
        self.sig_lines_deleted(self, start_line, end_line)

    def delete_range(self, start_line, start_offset, end_line, end_offset):
        _debug("Deleting from %s,%s to %s,%s", start_line, start_offset,
               end_line, end_offset)

        if self.state == NotebookFile.EXECUTING:
            return

        if start_line == end_line and start_offset == end_offset:
            return

        self.__freeze_changes()

        start_line, start_offset, end_line, end_offset = order_positions(
            start_line, start_offset, end_line, end_offset)

        deleted_text = self.get_text(start_line, start_offset, end_line,
                                     end_offset)

        self.sig_text_deleted(self, start_line, start_offset, end_line,
                              end_offset)

        if start_offset == 0 and end_offset == 0:
            # Deleting some whole number of lines
            self.__delete_lines(start_line, end_line)
        else:
            left = self.__lines[start_line][0:start_offset]
            right = self.__lines[end_line][end_offset:]

            if start_offset == 0:
                self.__delete_lines(start_line, end_line)
            else:
                self.__delete_lines(start_line + 1, end_line + 1)

            self.__set_line(start_line, left + right)
            chunk = self.__chunks[start_line]
            chunk.change_line(start_line)
            self.__chunk_changed(chunk)

        self.__thaw_changes()
        self.__undo_stack.append_op(
            DeleteOp((start_line, start_offset), (end_line, end_offset),
                     deleted_text))

        if self.__user_action_count > 0 and not self.code_modified:
            self.code_modified = True

    def place_cursor(self, line, offset):
        _debug("Place cursor at %s,%s", line, offset)
        self.sig_place_cursor(self, line, offset)

    def undo(self):
        self.__undo_stack.undo()

    def redo(self):
        self.__undo_stack.redo()

    def module_changed(self, module_name):
        """Mark statements for execution after a change to the given module"""

        for chunk in self.iterate_chunks():
            if not isinstance(chunk, StatementChunk):
                continue
            if chunk.statement is None:
                continue

            imports = chunk.statement.imports
            if imports is None:
                continue

            if imports.module_is_referenced(module_name):
                self.__mark_rest_for_execute(chunk.start)
                return

    def calculate(self, wait=False, end_line=None):
        _debug("Calculating")

        self.__freeze_changes()

        parent = None
        have_error = False

        executor = None

        for chunk in self.iterate_chunks(end_line=end_line):
            if isinstance(chunk, StatementChunk):
                if chunk.needs_compile or chunk.needs_execute:
                    if not executor:
                        executor = ThreadExecutor(parent)

                if executor:
                    statement = chunk.get_clean_statement(self)
                    executor.add_statement(statement)

                parent = chunk.statement

        # See if there are any more statements after the ones we are executing
        more_statements = (end_line is not None) and \
            any(isinstance(chunk, StatementChunk) for chunk in self.iterate_chunks(start_line=end_line))

        if executor:
            if wait:
                loop = executor.event_loop

            def on_statement_execution_state_changed(executor, statement):
                if (statement.state == Statement.COMPILE_ERROR
                        or statement.state == Statement.EXECUTE_ERROR
                        or statement.state == Statement.INTERRUPTED):
                    self.__executor_error = True

                statement.chunk.update_statement()

                if self.__freeze_changes_count == 0:
                    self.__freeze_changes()
                    self.__chunk_changed(statement.chunk)
                    self.__thaw_changes()
                else:
                    self.__chunk_changed(statement.chunk)

            def on_complete(executor):
                self.__executor.destroy()
                self.__executor = None
                if self.__executor_error:
                    self.__set_state(NotebookFile.ERROR)
                elif more_statements:
                    self.__set_state(NotebookFile.NEEDS_EXECUTE)
                else:
                    self.__set_state(NotebookFile.EXECUTE_SUCCESS)
                if wait:
                    loop.quit()

            self.__executor = executor
            self.__executor_error = False
            self.__set_state(NotebookFile.EXECUTING)
            executor.sig_statement_executing.connect(
                on_statement_execution_state_changed)
            executor.sig_statement_complete.connect(
                on_statement_execution_state_changed)
            executor.sig_complete.connect(on_complete)

            if executor.compile():
                executor.execute()
                if wait:
                    loop.run()
        else:
            # Nothing to execute, we could have been in a non-success state if statements were deleted
            # at the end of the file.
            if not more_statements:
                self.__set_state(NotebookFile.EXECUTE_SUCCESS)

        self.__thaw_changes()

    def interrupt(self):
        if self.state == NotebookFile.EXECUTING:
            self.__executor.interrupt()

    def __get_completion_scope(self, chunk):
        # Get the scope that we should use for completions for a given chunk; we
        # use the chunks own scope when possible because when we have something
        # like a build: statement, we will want to complete on variables not
        # in the previous scope.

        scope = None
        line = chunk.start
        while line >= 0:
            previous_chunk = self.__chunks[line]

            # We intentionally don't check "needs_execute" ... if there is a result scope,
            # it's fair game for completion/help, even if it's old
            if isinstance(
                    previous_chunk, StatementChunk
            ) and previous_chunk.statement is not None and previous_chunk.statement.result_scope is not None:
                return previous_chunk.statement.result_scope

            line = previous_chunk.start - 1

        return self.global_scope

    def find_completions(self, line, offset, min_length=0):
        """Returns a list of possible completions at the given position.

        Each element in the returned list is a tuple of (display_form,
        text_to_insert, object_completed_to)' where
        object_completed_to can be used to determine the type of the
        completion or get docs about it.

        @param min_length if supplied, the minimum length to require for an isolated
           name before we complete against the scope. This is useful if we are suggesting
           completions without the user explicitly requesting it.

        """

        chunk = self.__chunks[line]
        if not isinstance(chunk, StatementChunk) and not isinstance(
                chunk, BlankChunk):
            return []

        scope = self.__get_completion_scope(chunk)

        if isinstance(chunk, StatementChunk):
            return chunk.tokenized.find_completions(line - chunk.start,
                                                    offset,
                                                    scope,
                                                    min_length=min_length)
        else:
            # A BlankChunk Create a dummy TokenizedStatement to get the completions
            # appropriate for the start of a line
            ts = TokenizedStatement()
            ts.set_lines([''])
            return ts.find_completions(0, 0, scope, min_length=min_length)

    def get_object_at_location(self, line, offset, include_adjacent=False):
        """Find the object at a particular location within the worksheet

        @param include_adjacent: if False, then location identifies a character in the worksheet. If True,
           then location identifies a position between characters, and symbols before or after that
           position are included.

        @returns: a tuple of (object, start_line, start_offset, end_line, end_offset) or (None, None, None, None, None)

        """

        chunk = self.__chunks[line]
        if not isinstance(chunk, StatementChunk):
            return None, None, None, None, None

        if chunk.statement is not None and chunk.statement.result_scope is not None:
            result_scope = chunk.statement.result_scope
        else:
            result_scope = None

        obj, start_line, start_index, end_line, end_index = \
            chunk.tokenized.get_object_at_location(line - chunk.start, offset,
                                                   self.__get_completion_scope(chunk),
                                                   result_scope, include_adjacent)

        if obj is None:
            return None, None, None, None, None

        start_line += chunk.start
        end_line += chunk.start

        return obj, start_line, start_index, end_line, end_index

    def __do_clear(self):
        self.delete_range(0, 0,
                          len(self.__lines) - 1,
                          len(self.__lines[len(self.__lines) - 1]))

    def clear(self):
        self.__do_clear()
        self.__set_filename_and_modified(None, False)

        # XXX: This prevents redoing New, would that "just work"?
        self.__undo_stack.clear()

    def get_text(self,
                 start_line=0,
                 start_offset=0,
                 end_line=-1,
                 end_offset=-1):
        if start_line < 0:
            start_line = len(self.__lines) - 1
        if end_line < 0:
            end_line = len(self.__lines) - 1
        if start_offset < 0:
            start_offset = len(self.__lines[start_line])
        if end_offset < 0:
            end_offset = len(self.__lines[end_line])

        start_line, start_offset, end_line, end_offset = order_positions(
            start_line, start_offset, end_line, end_offset)

        if start_line == end_line:
            return self.__lines[start_line][start_offset:end_offset]

        si = StringIO()

        line = start_line
        si.write(self.__lines[line][start_offset:])
        line += 1

        while line < end_line:
            si.write("\n")
            si.write(self.__lines[line])
            line += 1

        si.write("\n")
        si.write(self.__lines[line][:end_offset])

        return si.getvalue()

    def get_doctests(self, start_line, end_line):
        si = StringIO()

        first = True
        for chunk in self.iterate_chunks(start_line, end_line + 1):
            for i in xrange(chunk.start, chunk.end):
                line_text = self.__lines[i]
                if isinstance(chunk, StatementChunk):
                    if i != chunk.start:
                        si.write("... ")
                    else:
                        si.write(">>> ")
                si.write(line_text)
                # Don't turn a trailing newline into two
                if i != len(self.__lines) - 1 or len(line_text) > 0:
                    si.write("\n")

            if isinstance(chunk, StatementChunk) and chunk.results is not None:
                for result in chunk.results:
                    if isinstance(result, basestring):
                        si.write(result)
                        si.write("\n")

        return si.getvalue()

    def get_line_count(self):
        return len(self.__lines)

    def get_chunk(self, line):
        return self.__chunks[line]

    def get_line(self, line):
        return self.__lines[line]

    #--------------------------------------------------------------------------------------
    # This should be a gobject.property, but we define filenames to be unicode strings
    # and it's impossible to have a unicode-string valued property. Unicode strings
    # set on a string gobject.property get endecoded to UTF-8. So, we use the separate
    # '::sig_filename_changed' signal.
    def __get_filename(self):
        return self.__filename

    def __set_filename(self, filename):
        if filename == self.__filename:
            return

        if self.__file:
            self.__file.worksheet = None
            self.__file.modified = False
            self.__file.active = False

        self.__filename = filename
        if filename:
            self.__file = self.notebook.file_for_absolute_path(self.__filename)
            if self.__file:
                self.__file.worksheet = self
                self.__file.active = True
                self.__file.modified = self.__code_modified
        else:
            self.__file = None
            pass

        self.sig_filename_changed(self, self.__filename)
        pass

    def __set_filename_and_modified(self, filename, modified):
        self.filename = filename
        self.code_modified = modified
        pass

    filename = property(__get_filename, __set_filename)

    #--------------------------------------------------------------------------------------
    def __get_file(self):
        return self.__file

    def __set_file(self, value):
        self.__file = value
        self.sig_file(self, self.__file)
        pass

    file = property(__get_file, __set_file)

    #--------------------------------------------------------------------------------------
    def __set_code_modified(self, code_modified):
        if code_modified == self.__code_modified:
            return

        self.__code_modified = code_modified
        if self.__file:
            self.__file.modified = code_modified
            pass

        self.sig_code_modified(self, self.__code_modified)
        pass

    def __get_code_modified(self):
        return self.__code_modified

    code_modified = property(__get_code_modified, __set_code_modified)

    #--------------------------------------------------------------------------------------
    def __get_state(self):
        return self.__state

    def __set_state(self, new_state):
        if self.edit_only:
            return
        self.__state = new_state
        if self.__file:
            self.__file.state = new_state
            pass

        self.sig_state(self, self.__state)
        pass

    state = property(__get_state, __set_state)

    #--------------------------------------------------------------------------------------
    def load(self, filename, escape=False):
        """Load a file from disk into the worksheet. Can raise IOError if the
        file cannot be read, and reunicode.ConversionError if the file contains
        invalid characters. (reunicode.ConversionError will not be raised if
        escape is True)

        @param filename the file to load
        @param escape if true, invalid byte and character sequences in the input
           will be converted into \\x<nn> and \\u<nnnn> escape sequences.

        """
        if not isinstance(filename, unicode):
            raise ValueError("filename argument must be unicode")
        f = open(filename)
        text = f.read()
        f.close()

        self.__do_clear()
        self.insert(0, 0, reunicode.decode(text, escape=escape))
        # A bit of a hack - we assume that if escape was passed we *did* escape.
        # this is the way that things work currently - first the GUI loads with
        # escape=False, and if that fails, prompts the user and loads with escape=True
        self.__set_filename_and_modified(filename, escape)
        self.__undo_stack.clear()

    def save(self, filename=None):
        if not isinstance(filename, unicode):
            raise ValueError("filename argument must be unicode")
        if filename is None:
            if self.__filename is None:
                raise ValueError("No current or specified filename")

            filename = self.__filename

        if not self.code_modified and filename == self.__filename:
            return

        filename_changed = filename != self.__filename

        tmpname = filename + ".tmp"

        # We use binary mode, since we don't want to munge line endings to the system default
        # on a load-save cycle
        f = open(tmpname, "wb")

        success = False
        try:
            first = True
            for line in self.__lines:
                if not first:
                    f.write("\n")
                first = False
                f.write(line.encode("utf8"))

            f.close()
            # Windows can't save over an existing filename; we might want to check os.name to
            # see if we have to do this, but it's unlikely that the unlink will succeed and
            # the rename fail, so I think it's 'atomic' enough this way.
            if os.path.exists(filename):
                os.unlink(filename)
            os.rename(tmpname, filename)
            success = True

            # Need to refresh the notebook before saving so that we find the NotebookFile
            # properly in __set_filename_and_modified
            if filename_changed:
                self.notebook.refresh()
            self.__set_filename_and_modified(filename, False)
            if self.notebook.info:
                self.notebook.info.update_last_modified()
        finally:
            if not success:
                f.close()
                try:
                    os.remove(tmpname)
                except:
                    pass