Commits

Henning Schröder  committed 6dfb131

code cleanup, configurable color schemes, speed optimizations for callbacks, small fixes/renames/..

  • Participants
  • Parent commits b0d7c88

Comments (0)

Files changed (13)

File codeaide/base.py

     QColor, QBrush, QPainter, QPen, QPalette, QTextEdit, QTextFormat)
 
 
-
-def command(func):
-    """
-    decorator to declare a method as an editor command
-    """
-    func.is_command = True
-    return func
-
-
-
-def class_members(cls):
-    names = dir(cls)
-    if hasattr(cls,'__bases__'):
-        for base in cls.__bases__:
-            names.extend(class_members(base))
-    return list(set(names))
-
-
-def members(obj):
-    names = dir(obj)
-    cls = getattr(obj, "__class__")
-    names.extend(class_members(cls))
-    return list(set(names))
-
     
 
 class ModeBase(object):
 
     keyboard_hooks = {
         }
+    paint_hook_before = None
     paint_hook = None
+    paint_hook_after = None
     resize_hook = None
+    
     state = 0
-    name = None
+
     requires = []
 
 
-    
+
     @classmethod
     def find(cls, mode):
         for subcls in cls.__subclasses__():
 
     def __init__(self, textedit, *args, **kwargs):
         for required_mode in self.requires:
-            textedit.install(required_mode)
+            textedit.require(required_mode)
         self.textedit = textedit
         self.init(*args, **kwargs)
         self.install_hooks()
 
 
     def install_paint_hook(self):
-        callback = self.paint_hook
+        callback = self.paint_hook_before or self.paint_hook 
         if callback:
-            bound_method = callback
-            #bound_method = \
-            #    lambda *args, **kwargs: callback(self, *args, **kwargs)
-            h = self.textedit.paint_hooks
-            h.setdefault(self.state, []).append(bound_method)
+            h = self.textedit.paint_hooks_before
+            h.setdefault(self.state, []).append(callback)
+        callback = self.paint_hook_after
+        if callback:
+            h = self.textedit.paint_hooks_after
+            h.setdefault(self.state, []).append(callback)
 
         
     def install_resize_hook(self):
         callback = self.resize_hook
         if callback:
-            bound_method = callback
             h = self.textedit.resize_hooks
-            h.setdefault(self.state, []).append(bound_method)
+            h.setdefault(self.state, []).append(callback)
+
+
+
+
+class TextCursor(QTextCursor):
+
+
+    def __enter__(self, *args):
+        self.beginEditBlock()
+
+
+    def __exit__(self, *args):
+        self.endEditBlock()
+
+
+    def current_word(self):
+        self.select(QTextCursor.WordUnderCursor)
+        return unicode(self.selectedText())
+
+
+    def current_line(self):
+        return unicode(self.block().text())
+
+
+    def endswith_word(self):
+        word = self.current_word()
+        pos = self.position()
+        line = self.current_line()
+        if line[:pos].endswith(word):
+            return word
+
+
+    def select_range(self, start_pos, end_pos):
+        self.setPosition(start_pos)
+        self.setPosition(end_pos, QTextCursor.KeepAnchor)
+
+
+    def remove_range(self, start_pos, end_pos):
+        self.select_range(start_pos, end_pos)
+        self.removeSelectedText()
+

File codeaide/modes/__init__.py

 
 # XXX: importing from a zip package does not work
 
-modes = []
+module_names = []
 
 def find():
-    global modes
-    modes = []
+    global module_names
+    module_names = []
     #path = os.path.dirname(os.path.abspath(__file__))
     for path in __path__:
         for name in os.listdir(path):
             if re.match("^[A-Za-z][A-ZA-z0-9]*\.py$", name):
-                modes.append(os.path.splitext(name)[0])
+                module_names.append(os.path.splitext(name)[0])
 
                 
 def add_path(path):
+    global module_names
+    module_names = [] 
     __path__.insert(0, path)
+
     
         
 def load(name):
 
 
 def load_all(verbose=False):
-    if not modes:
+    if not module_names:
         find()
-    for name in modes:
+    for name in module_names:
         if verbose:
-            print >> sys.stderr, "Loading mode %r" % name
+            print >> sys.stderr, "Loading modes from codeaide.modes.%s" % name
         load(name)

File codeaide/modes/completer.py

         pass
 
 
-    @command
     def textUnderCursor(self):
         tc = self.textedit.textCursor()
         tc.select(QTextCursor.WordUnderCursor)
         return tc.selectedText()
 
 
-    @command
     def insertCompletion(self, completion):
         tc = self.textedit.textCursor()
         c = self.widget

File codeaide/modes/folding.py

         QWidget.__init__(self, parent)
 
 
-    def paintEvent(self, event):
+    def xpaintEvent(self, event):
         painter = QPainter(self)
         b = QBrush(QColor("white"))
         painter.fillRect(event.rect(), b)
 
 
     def sizeHint(self):
-        return QSize(20, 0)
+        return QSize(11, 0)
 
 
 

File codeaide/modes/indenter.py

 class IndenterBase(ModeBase):
 
 
-    @command
     def correct_indentation(self, line_number=-1):
         if line_number == -1:
             cursor = self.textedit.textCursor()
             line_number = cursor.blockNumber()
 
 
-    @command
     def entering_new_line(self, line_number=-1):
         if line_number == -1:
             cursor = self.textedit.textCursor()
 
 
 
-    @command
     def dedent(self):
         cursor = self.textedit.textCursor()
         text = unicode(cursor.block().text())
 
 
 
-    @command
     def indent(self):
         self.indenter.correct_indentation()
 

File codeaide/modes/lines.py

-from codeaide.base import *
-
-
-class VerticalLine(ModeBase):
-
-
-    def init(self):
-        self.width = 78
-        self.x = self.textedit.fontMetrics().width("W"*78)
-        self.color = QColor("lightgrey")
-        self.pen = QPen(self.color)
-        self.brush = QBrush(QColor(self.color.lighter(115)))
-
-
-    def resize_hook(self, event):
-        cr = self.textedit.contentsRect()
-        y = cr.top()
-        x = cr.left() + self.x
-        h = cr.height()
-        w = cr.width()
-        self.line = QLine(x, y, x, h)
-        self.rect = QRect(x+1, y, w, h)
-
-    
-    def paint_hook(self, event, painter, viewport):
-        rect = event.rect()
-        if rect.right() < self.x:
-            return
-        painter.setPen(self.pen)
-        painter.drawLine(self.line)
-        painter.fillRect(self.rect, self.brush)
-
-
-        
-class HighlightLine(ModeBase):
-    """
-    Originally drawing was done in a paint event by fill a rect.
-    Now this mode uses an ExtraSelection (Is this better?)
-    """
-
-
-    def init(self):
-        #color = self.textedit.palette().highlight().color().lighter(175)
-        color = QColor("blue").lighter(190)
-        self.brush = QBrush(QColor(color))
-        self.textedit.connect(self.textedit, 
-                              SIGNAL("cursorPositionChanged()"),
-                              self.update)
-        self.old_row = -1
-
-
-    def update(self):
-        cursor = self.textedit.textCursor()
-        row = cursor.blockNumber()
-        if row != self.old_row:
-            # XXX: self.textedit.viewport().update()
-            self.old_row = row
-            
-            self.sel = QTextEdit.ExtraSelection()
-            self.sel.cursor = QTextCursor(self.textedit.textCursor())
-            self.sel.format.setBackground(self.brush)
-            self.sel.format.setProperty(QTextFormat.FullWidthSelection, QVariant(True))
-            self.sel.cursor.select(QTextCursor.LineUnderCursor)
-            self.textedit.setExtraSelections([self.sel])
-
-    
-    def XXXpaint_hook(self, event, painter, viewport):
-        r = self.textedit.cursorRect()
-        r.setX(0)
-        r.setWidth(viewport.width())
-        painter.fillRect(r, self.brush)

File codeaide/modes/occurences.py

-import re
-from codeaide.base import *
-
-
-is_word = re.compile("^\w+$").match
-
-
-class Occurences(ModeBase):
-
-
-    def init(self, wait_seconds=2):
-        self.old_pos = -1
-        self.brush = QBrush(QColor("yellow"))
-        self.selections = []
-        self.textedit.connect(self.textedit, SIGNAL("idle()"), self.on_idle)
-
-        
-    def highlight_word_in_block(self, word, block):
-        block_pos = block.position()
-        text = unicode(block.text())
-        length = len(word)
-        i = 0
-        while True:
-            found = text.find(word, i)
-            if found == -1:
-                break
-            col = block_pos + found
-            sel = QTextEdit.ExtraSelection()
-            sel.cursor = QTextCursor(block)
-            sel.cursor.setPosition(col)
-            sel.cursor.setPosition(col+length, QTextCursor.KeepAnchor)
-            #sel.cursor.select(QTextCursor.WordUnderCursor)
-            sel.format.setBackground(self.brush)
-            self.selections.append(sel)
-            i += len(word)
-
-
-    def on_idle(self):
-        self.highlight_current_word()
-
-        
-    def highlight_word(self, word):
-        # This maybe also used by a search function
-        self.textedit.remove_extra_selections(self.selections)
-        self.selections = []
-
-        block = self.textedit.firstVisibleBlock()
-        bounding_rect = self.textedit.blockBoundingRect
-        y = 0
-        height = self.textedit.height()
-        while block and block.isValid() and y < height:
-            if not block.isVisible():
-                continue
-            self.highlight_word_in_block(word, block)
-            y += bounding_rect(block).height()
-            block = block.next()
-        self.textedit.add_extra_selections(self.selections)
-
-
-    def highlight_current_word(self):
-        cursor = self.textedit.textCursor()
-        pos = cursor.position()
-        if pos == self.old_pos:
-            return
-        self.old_pos = pos
-
-        cursor.select(QTextCursor.WordUnderCursor)
-        word = unicode(cursor.selectedText())
-        if not word or not is_word(word):
-            return
-        self.highlight_word(word)
-
-        
-    def on_highlight_current_word(self, event, key, modifiers, text):
-        self.highlight_current_word()
-
-
-    keyboard_hooks = {
-        (Qt.Key_O, Qt.ControlModifier): on_highlight_current_word,
-        }

File codeaide/modes/occurrences.py

+import re
+from codeaide.base import *
+
+
+is_word = re.compile("^\w+$").match
+
+
+class Occurrences(ModeBase):
+
+
+    def init(self, wait_seconds=2):
+        self.old_pos = -1
+        self.color = QColor(self.textedit.scheme.occurrence_color)
+        self.brush = QBrush(QColor(self.textedit.scheme.occurrence_background))
+        self.selections = []
+        self.textedit.connect(self.textedit, SIGNAL("idle()"), self.on_idle)
+
+        
+    def highlight_word_in_block(self, word, block):
+        block_pos = block.position()
+        text = unicode(block.text())
+        length = len(word)
+        i = 0
+        while True:
+            found = text.find(word, i)
+            if found == -1:
+                break
+            col = block_pos + found
+            sel = QTextEdit.ExtraSelection()
+            sel.cursor = QTextCursor(block)
+            sel.cursor.setPosition(col)
+            sel.cursor.setPosition(col+length, QTextCursor.KeepAnchor)
+            #sel.cursor.select(QTextCursor.WordUnderCursor)
+            sel.format.setForeground(self.color)
+            sel.format.setBackground(self.brush)
+            self.selections.append(sel)
+            i += len(word)
+
+
+    def on_idle(self):
+        self.highlight_current_word()
+
+        
+    def highlight_word(self, word):
+        # This maybe also used by a search function
+        self.textedit.remove_extra_selections(self.selections)
+        self.selections = []
+
+        block = self.textedit.firstVisibleBlock()
+        bounding_rect = self.textedit.blockBoundingRect
+        y = 0
+        height = self.textedit.height()
+        while block and block.isValid() and y < height:
+            if not block.isVisible():
+                continue
+            self.highlight_word_in_block(word, block)
+            y += bounding_rect(block).height()
+            block = block.next()
+        self.textedit.add_extra_selections(self.selections)
+
+
+    def highlight_current_word(self):
+        cursor = self.textedit.textCursor()
+        pos = cursor.position()
+        if pos == self.old_pos:
+            return
+        self.old_pos = pos
+
+        cursor.select(QTextCursor.WordUnderCursor)
+        word = unicode(cursor.selectedText())
+        if not word or not is_word(word):
+            return
+        self.highlight_word(word)
+
+        
+    def on_highlight_current_word(self, event, key, modifiers, text):
+        self.highlight_current_word()
+
+
+    keyboard_hooks = {
+        (Qt.Key_O, Qt.ControlModifier): on_highlight_current_word,
+        }

File codeaide/modes/sidebar.py

     
     def init(self, SidebarWidgetClass=SidebarWidget):
         self.widget = SidebarWidgetClass(self.textedit)
-        self.fm = self.textedit.fontMetrics()
-        self.blocks_changed(0)
         self.textedit.connect(
-            self.textedit, SIGNAL("blockCountChanged(int)"), 
-                self.blocks_changed)
-        self.textedit.connect(self.textedit.verticalScrollBar(),
-                   SIGNAL("valueChanged(int)"),
-                   self.blocks_changed)
+            self.textedit, SIGNAL("visible_blocks_changed()"), 
+                self.on_visible_blocks_changed)
 
 
-    def blocks_changed(self, num):
-        lines = []
-        te = self.textedit
-        block = te.firstVisibleBlock()
-        row = block.blockNumber() + 1
-        width = self.widget.width()
-        w  = width -2
-        h = self.fm.height()
-
-        bbgeom = te.blockBoundingGeometry(block)
-        top = bbgeom.translated(te.contentOffset()).top()
-        bottom = top + bbgeom.height()
-        ebottom_top = 0 #event.rect().top()
-        ebottom_bottom = self.widget.height() #event.rect().bottom()
-        while block.isValid() and top <= ebottom_bottom:
-            if block.isVisible() and bottom >= ebottom_top:
-                lines.append(
-                   ((row, block), (0, top, w, h))
-                   )
-            block = block.next()
-            row += 1
-            top = bottom
-            bottom = top + te.blockBoundingRect(block).height()
-        self.lines = lines
+    def on_visible_blocks_changed(self):
         self.widget.emit(SIGNAL("updateSidebar()"))
 
 
 
     
     def __init__(self, mode):
-        QWidget.__init__(self, mode.textedit)
+        self.textedit = mode.textedit
+        QWidget.__init__(self, self.textedit)
         self.sidebar = mode.sidebar
         mode.sidebar_widget.connect(
             mode.sidebar_widget, SIGNAL("updateSidebar()"), self.update)
-        self.textpen = QPen(QColor(0x80, 0x80, 0x80))
+        self.text_pen = QPen(QColor(self.textedit.scheme.line_number_color))
+        self.back_brush = QBrush(
+            QColor(self.textedit.scheme.line_number_background))
 
 
     def sizeHint(self):
 
     def paintEvent(self, event):
         painter = QPainter(self)
-        painter.setPen(self.textpen)
-        w = self.width()
-        for (row, block), (left, top, width, height) in self.sidebar.lines: 
+        painter.fillRect(event.rect(), self.back_brush)
+        painter.setPen(self.text_pen)
+        w = self.width() - 2
+        for vb in self.textedit.visible_blocks: 
             painter.drawText(
-                    0, top, w, height, Qt.AlignRight, str(row))
+                    0, vb.top, w, vb.height, Qt.AlignRight, str(vb.row))
         return QWidget.paintEvent(self, event)
 
 
         self.textedit = mode.textedit
         QWidget.__init__(self, self.textedit)
         self.modified_pen = QPen(QColor("red"))
-        self.textedit.connect(self.textedit, SIGNAL("textChanged()"), 
-            self.on_text_changed)
+        self.connect(self.textedit, SIGNAL("newText()"),
+                              self.on_new_text)
+        self.connect(self.textedit.document(), SIGNAL("contentsChange(int,int,int)"),
+                     self.on_contents_change)
         self.setMouseTracking(True)
         self.sidebar_widget.connect(
             self.sidebar_widget, SIGNAL("updateSidebar()"), self.update)
 
 
+    def on_new_text(self):
+        block = self.textedit.document().begin()
+        while block and block.isValid():
+            user_data = self.textedit.user_data(block)
+            user_data["status"] = 0
+            user_data["revision"] = 0
+            user_data["backup"] = block.text()
+            block = block.next()
+
+
     def mouseMoveEvent(self, event):
         pos = event.pos()
         y = pos.y()
-        for (row, block), (left, top, width, height) in self.sidebar.lines:
+        for vb in self.textedit.visible_blocks:
+            left, top, width, height =  vb.rect
             if y > top and y < top+height:
-                user_data = block.userData()
+                user_data = vb.block.userData()
                 if not user_data:
                     return
                 backup = user_data.get("backup")
                     return
                 QToolTip.showText(
                     self.mapToGlobal(pos), backup, self)
-                
 
 
-    def on_text_changed(self):
-        # XXX: does not fully work with selecions
-        cursor = self.textedit.textCursor()
-        block = cursor.block()
-        previous = block.previous()
-        if previous and previous.isValid():
-            pud = self.textedit.user_data(previous)
-            if pud.get("backup") != previous.text():
-                pud["status"] = 1
-        user_data = self.textedit.user_data(block)
-        user_data["status"] = 1
-        self.update()   
+
+    def on_contents_change(self, position, chars_removed, chars_added):
+        if chars_removed == 0 and chars_added == 0:
+            # style changes (highlighting, ..)
+            return
+        textedit = self.textedit
+        doc = textedit.document()
+        start_block = doc.findBlock(position - chars_removed)
+        end_block = doc.findBlock(position + chars_added)
+        block = start_block
+        update = False
+        while block and block.isValid():
+            user_data = textedit.user_data(block)
+            if user_data.get("backup") != block.text():
+                if user_data.get("status", 0) != 1:
+                    user_data["status"] = 1
+                    user_data["revision"] = user_data.get("revision", 0) + 1
+                    update = True
+            if block == end_block:
+                break
+            block = block.next()
+        # XXX: updating only changed area might be slower
+        # because of more Python calls?
+        if update:
+            self.update()
 
 
     def sizeHint(self):
     def paintEvent(self, event):
         painter = QPainter(self)
         painter.setPen(self.modified_pen)
-        for (row, block), (left, top, width, height) in self.sidebar.lines:
-            user_data = block.userData()
+        for vb in self.textedit.visible_blocks:
+            user_data = vb.block.userData()
             if not user_data or user_data["status"] == 1:
-                painter.drawLine(left, top, left, top+height)
-                painter.drawLine(left+1, top, left+1, top+height)
+                left, top, right, height = vb.rect
+                # XXX: check if in event.rect
+                painter.drawLine(0, top, 0, top+height)
+                painter.drawLine(1, top, 1, top+height)
         return QWidget.paintEvent(self, event)
 
 

File codeaide/modes/visualhelper.py

+from codeaide.base import *
+
+
+class WrapIndicator(ModeBase):
+
+
+    def init(self, width=80):
+        self.x = self.textedit.fm.width("0"*width)
+        self.pen = QPen(QColor(self.textedit.scheme.wrap_indicator_color))
+        self.brush = QBrush(QColor(self.textedit.scheme.wrap_indicator_background))
+
+
+    def resize_hook(self, event):
+        cr = self.textedit.contentsRect()
+        y = cr.top()
+        x = cr.left() + self.x
+        h = cr.height()
+        w = cr.width()
+        self.line = QLine(x, y, x, h)
+        self.rect = QRect(x+1, y, w, h)
+
+    
+    def paint_hook_before(self, event, painter, viewport):
+        rect = event.rect()
+        if rect.right() < self.x:
+            return
+        painter.fillRect(self.rect, self.brush)
+
+
+    def paint_hook_after(self, event, painter, viewport):
+        rect = event.rect()
+        if rect.right() < self.x:
+            return
+        painter.setPen(self.pen)
+        painter.drawLine(self.line)
+
+
+
+        
+class HighlightLine(ModeBase):
+    """
+    Originally drawing was done in a paint event with fillRect.
+    Now this mode uses an ExtraSelection (Is this better?)
+    """
+
+
+    def init(self):
+        self.old_row = -1
+        self.selection = None
+        self.brush = QBrush(QColor(self.textedit.scheme.highlight_line_background))
+        self.textedit.connect(self.textedit, 
+                              SIGNAL("cursorPositionChanged()"),
+                              self.update)
+
+
+
+    def update(self):
+        cursor = self.textedit.textCursor()
+        row = cursor.blockNumber()
+        if row != self.old_row:
+            self.old_row = row
+            sel = self.selection
+            if sel:
+                self.textedit.remove_extra_selections([sel])
+            sel = QTextEdit.ExtraSelection()
+            sel.cursor = QTextCursor(self.textedit.textCursor())
+            sel.format.setBackground(self.brush)
+            sel.format.setProperty(QTextFormat.FullWidthSelection, QVariant(True))
+            self.textedit.add_extra_selections([sel])
+            self.selection = sel
+
+    
+    #def paint_hook(self, event, painter, viewport):
+    #    r = self.textedit.cursorRect()
+    #    r.setX(0)
+    #    r.setWidth(viewport.width())
+    #    painter.fillRect(r, self.brush)

File codeaide/widget.py

 from codeaide.base import *
 
 
+
+
+
 class TextBlockUserData(QTextBlockUserData):
     
     def __init__(self):
         QTextBlockUserData.__init__(self)
         self.attrs = {}
-        
+
+    def __repr__(self):
+        return "<%s object at 0x%x: %r>" % (self.__class__.__name__, id(self), self.attrs)
+
         
     def __getitem__(self, name):
         return self.attrs[name]
 
 
 
+class VisibleBlock(object):
+
+    def __init__(self, row, block, rect):
+        self.row = row
+        self.block = block
+        self.rect = (self.left, self.top, self.width, self.height) = rect
+
+
+
+class Scheme(object):
+
+    def __init__(self,
+                 color="black",
+                 background="white",
+                 selection_color="white",
+                 selection_background="#437DCD",
+                 font_name="Monospace",
+                 font_size=10,
+                 line_number_color="#866D71",
+                 line_number_background="#E1E1E1",
+                 highlight_line_background="#E4EDF8",
+                 wrap_indicator_color="#D8D8D8",
+                 wrap_indicator_background="#F7F7F7",
+                 occurrence_color="black",
+                 occurrence_background="yellow",
+                 ):
+        self.color = color
+        self.background = background
+        self.selection_color = selection_color
+        self.selection_background = selection_background
+        self.font_name = font_name
+        self.font_size = font_size
+        self.line_number_color = line_number_color
+        self.line_number_background = line_number_background
+        self.highlight_line_background = highlight_line_background
+        self.wrap_indicator_color = wrap_indicator_color
+        self.wrap_indicator_background = wrap_indicator_background
+        self.occurrence_color = occurrence_color
+        self.occurrence_background = occurrence_background
+
+
+    def __getitem__(self, name):
+        return self.__dict__[name]
+
+
+
 class CodeAide(QPlainTextEdit):
 
 
-
     def __init__(self, parent=None):
         QPlainTextEdit.__init__(self, parent)
         self.setFrameStyle(0)
-        #self.setBackgroundVisible(True)
-        self.set_font()
-        self.state = 0
+        self.set_scheme()
         self.keyboard_hooks = {}
-        self.paint_hooks = {}
+        self.paint_hooks = self.paint_hooks_before = {}
+        self.paint_hooks_after = {}
         self.resize_hooks = {}
         self.installed_modes = []
         self.idle_timer_id = self.startTimer(2000)
+        self.visible_blocks = []
+        self.extra_selections = []
+        self.connect(
+            self, SIGNAL("blockCountChanged(int)"), self.on_blocks_changed)
+        self.connect(self.verticalScrollBar(),
+                   SIGNAL("valueChanged(int)"), self.on_blocks_changed)
+        self.connect(self, SIGNAL("newText()"), self.on_blocks_changed)
+
+
+    def set_scheme(self, scheme=Scheme()):
+        css = """
+          QPlainTextEdit {
+             font-family: %(font_name)s;
+             font-size: %(font_size)spt;
+             color: %(color)s;
+             background-color: %(background)s;
+             selection-color: %(selection_color)s;
+             selection-background-color: %(selection_background)s;
+        }""" % scheme
+        self.setStyleSheet(css)
+        self.fm = self.fontMetrics()
+        self.scheme = scheme
 
 
     def not_idle(self):
         self.emit(SIGNAL("idle()"))
 
 
-    def set_font(self, name="Monospace", size=10):
-        font = self.document().defaultFont()
-        font.setPointSize(10)
-        font.setFamily("Monospace")
-        font.setFixedPitch(True)
-        self.document().setDefaultFont(font)
-        self.setFont(font)
-
-
     def find_mode(self, cls):
         for mode in self.installed_modes:
             if isinstance(mode, cls):
             if not self.is_installed(mode):
                 mode_instance = mode(self)
                 self.installed_modes.append(mode_instance)
+                setattr(self, mode.__name__, mode_instance)
+        self.switch_state(0)
 
+    require = install #alias for emacs fans ;)
 
-    @property
-    def commands(self):
-        c = {}
-        for mode in self.installed_modes:
-            for name in members(mode):
-                if not name.startswith("_"):
-                    obj = getattr(mode, name)
-                    if getattr(obj, "is_command", False):
-                        c[name] = obj
-        return c
 
-
-    def __call__(self, cmd, *args, **kwargs):
-        method = self.commands.get(cmd)
-        if not method:
-            raise TypeError, "Method %r not found" % cmd
-        return method(*args, **kwargs)
+    def switch_state(self, state):
+        self.state = state
+        # rearrage some data for faster access in events
+        self._paint_hooks_before = tuple(
+            self.paint_hooks_before.get(self.state, []) + \
+            self.paint_hooks_before.get(-1, []))
+        self._paint_hooks_after = tuple(
+            self.paint_hooks_after.get(self.state, []) + \
+            self.paint_hooks_after.get(-1, []))
+        
+        h = self._keyboard_hooks = dict()
+        for state in [self.state, -1]:
+            for keys, callbacks in self.keyboard_hooks.get(state, {}).items():
+                print keys
+                h.setdefault(keys, []).extend(callbacks)
 
 
     def paintEvent(self, event):
         viewport = self.viewport()
-        painter = QPainter(viewport)
-        h = self.paint_hooks
-        for callback in \
-                h.get(self.state, []) + h.get(-1, []):
-            if callback(event, painter, viewport):
-                return
-        painter.end()
-        return QPlainTextEdit.paintEvent(self, event)
+        before = self._paint_hooks_before
+        if before:
+            painter = QPainter(viewport)
+            for callback in before:
+                if callback(event, painter, viewport):
+                    return
+            painter.end()
+        QPlainTextEdit.paintEvent(self, event)
+        after = self._paint_hooks_after
+        if after:
+            painter = QPainter(viewport)
+            for callback in after:
+                if callback(event, painter, viewport):
+                    return
+            painter.end()
 
-    
+
     def mousePressEvent(self, event):
         self.not_idle()
         return QPlainTextEdit.mousePressEvent(self, event)
         key = event.key()
         text = event.text()
         modifiers = event.modifiers()
-        h = self.keyboard_hooks.get(self.state, {})
-        for callback in \
-                h.get((key, int(modifiers)), []) + \
-                h.get((key, -1), []):
-            if callback(event, key, modifiers, text):
-                return
+        keyboard_hooks = self._keyboard_hooks
+        if keyboard_hooks:
+            h = keyboard_hooks.get((key, modifiers), []) + \
+                keyboard_hooks.get((key, -1), []) + \
+                keyboard_hooks.get((-1, modifiers), []) + \
+                keyboard_hooks.get((-1, -1), [])
+            for callback in h:
+                if callback(event, key, modifiers, text):
+                    return
         self.not_idle()
         return QPlainTextEdit.keyPressEvent(self, event)
 
             block.setUserData(user_data)
         return user_data
 
-    
-    def mark_unmodified(self):
-        # XXX this should be done inside line status mode
-        block = self.document().begin()
-        while block and block.isValid():
-            user_data = self.user_data(block)
-            user_data["status"] = 0
-            user_data["backup"] = block.text()
-            block = block.next()
 
-
-    def setText(self, text):
-        self.setPlainText(text)
-        #self.emit("newText")
-        self.mark_unmodified()
+    def setPlainText(self, text):
+        QPlainTextEdit.setPlainText(self, text)
+        self.emit(SIGNAL("newText()"))
 
 
     def remove_extra_selections(self, selections):
         l = []
-        for sel in self.extraSelections():
-            if not sel in selections:
+        extras = self.extra_selections
+        for sel in selections:
+            if sel in extras:
+                extras.remove(sel)
+            else:
                 l.append(sel)
         self.setExtraSelections(l)
-            
+
 
     def add_extra_selections(self, selections):
-        self.setExtraSelections(self.extraSelections() + selections)
+        self.extra_selections.extend(selections)
+        self.setExtraSelections(self.extra_selections)
 
 
+    def on_blocks_changed(self, num=-1):
+        visible_blocks = []
+        block = self.firstVisibleBlock()
+        row = block.blockNumber() + 1
+        width = self.width()
+        w  = width -2
+        h = self.fm.height()
+
+        bbgeom = self.blockBoundingGeometry(block)
+        top = bbgeom.translated(self.contentOffset()).top()
+        bottom = top + bbgeom.height()
+        ebottom_top = 0 #event.rect().top()
+        ebottom_bottom = self.height() #event.rect().bottom()
+        while block.isValid() and top <= ebottom_bottom:
+            if block.isVisible() and bottom >= ebottom_top:
+                visible_blocks.append(
+                   VisibleBlock(row, block, (0, top, w, h))
+                   )
+            block = block.next()
+            row += 1
+            top = bottom
+            bottom = top + self.blockBoundingRect(block).height()
+        self.visible_blocks = visible_blocks
+        self.emit(SIGNAL("visible_blocks_changed()"))
+
 dispatch all key events depending on state
 clean up completer, async requests
 implement indenter
-paren matching highligher
+paren pair matching highligher
 same word highlighter
 snippets: loader for gedit and textmate, integrate with completer
 add syntax highlighter (also load from textmate bundles?)
 flymake-mode with pylint implementation (threading!)
-get inspiration from http://nschum.de/src/emacs/
+get inspiration from http://nschum.de/src/emacs/
+search with occurences
+instant rename based on snippets and occurences

File test_editor.py

     import sys
     app = QApplication(sys.argv)
     edit = CodeAide()
-    src = open(__file__).read()
-    edit.setText(src)
     edit.install(
         "AutoTyping", 
-        "VerticalLine",
+        "WrapIndicator",
         "HighlightLine",
         "LineNumbers",  # installs Sidebar as a dependency
         "LineStatus",
         "Folding",
         "Snippets",
         "TextCompleter",
-        "Occurences",
+        "Occurrences",
         )
     edit.resize(800,600)
     edit.show()
+    src = open(__file__).read()
+    edit.setPlainText(src)
+
     app.exec_()