[gedit-list] [CODE] Hacking on new gedit plugin "SmarterSpaces"



Hi Paolo et al.,

Following our quick discussion on IRC, I've been up hacking and
fighting[1] with gedit and gedit plugins. Suffice it to say, that I am
grateful I can write plugins in python, and don't have to go the
gnome-shell javascript route ;) Thanks for getting me started!

To make a long story short, I've put together a fairly functional new
plugin: "SmarterSpaces". This is a fork of Steve FrÃcinaux's excellent
"SmartSpaces" plugin. I am extending the code significantly past his
original concept, however I call it a fork since I have renamed it so as
not to conflict with the existing "SmartSpaces" plugin. I am more than
happy to submit a patch to merge my code back in if everyone likes this.

What my plugin does:
I *really* can't stand using spaces for code indentation, it's insane.
However, a lot of people out there do this, so I'd like to get my spaces
to pretend that they are really tabs for when I hack on other peoples
code. This means that when I use the left and right arrow keys, I expect
the cursor to "jump" over the tabwidth of spaces until the next tabstop.

If for some reason you used the mouse to put the cursor in the "middle"
of a tab, moving left or right should take you to its boundary.

This needs to work at the start of a line (where indentation usually is)
but also at the end of lines where comments hide, and in the middle
where more than one spaces are hanging out together. (The last two use
cases were harder.)

All of this should still work during "selection", when used with shift,
or control+shift. (I had to add this in, but it was a surprisingly easy
hack.)

At the moment, this seems to be working for me. I'd really appreciate if
you would test this, and let me know if you find any bugs, corner cases,
or have suggestions.

This caused me a lot of "off-by-one" hell, but I think it was worth it.
There are one or two places in the code where I put some XXX, because I
would need your advice. For some reason, I'm not 100% sure the
boilerplate is perfect. Maybe someone can confirm that loading/unloading
works properly, as I'm not sure. Thank you in advance.

I've tested this with gedit 3.6.2

Thanks to pbor for telling me this might be hackish and impossible, and
to shaddyz who helped me find the "doc" and "view" objects :)

Happy hacking,
James

[1] good fighting :P

[Plugin]
Loader=python
Module=smarterspaces
IAge=3
Name=Smarter Spaces
Name[ar]=ÙØØÙØØ ØÙÙØ
Name[be]=ÐÐÐÑÐÐÑÑ ÐÑÐÐÐÐÑ
Name[cs]=Inteligentnà mezery
Name[da]=Smarte mellemrum
Name[de]=Intelligente Leerzeichen
Name[el]=ÎÎÏÏÎÎ ÎÎÎÏÏÎÎÎÏÎ
Name[en_GB]=Smart Spaces
Name[es]=Espacios inteligentes
Name[eu]=Tarte azkarrak
Name[fr]=Espaces intelligents
Name[gl]=Espazos intelixentes
Name[he]=××××××× ×××××
Name[hu]=Intelligens szÃkÃzÃk
Name[id]=Spasi Cerdas
Name[it]=Spazi intelligenti
Name[ja]=ãããããããã
Name[lt]=IÅmanieji tarpai
Name[lv]=GudrÄs atstarpes
Name[pl]=Inteligentne spacje
Name[pt]=EspaÃos Inteligentes
Name[pt_BR]=EspaÃos inteligentes
Name[ro]=SpaÈii inteligente
Name[ru]=ÂÐÐÐÑÐÂ ÐÑÐÐÐÐÑ
Name[sk]=Inteligentnà medzery
Name[sl]=Pametni presledki
Name[sr]=ÐÐÐÐÑÐÐ ÑÐÐÐÐÑÐ
Name[sr latin]=Pametni razmaci
Name[zh_CN]=æèçæ
Description=Really forget you're not using tabulations.
Description[ar]=ØÙØ ØÙÙ ÙØ ØØØØØÙ ØÙØØÙÙØ.
Description[as]=ààààà àààààààà ààààààà àààààà ààààà àààà à
Description[be]=ÐÐÐÑÐÐÑÑÐ ÐÑÐ ÑÐÐ, ÑÑÐ ÐÑ ÐÐ ÑÐÑÐÐÐÑÐ ÑÐÐÑÐÑÑÑÑ.
Description[be latin]=ZabudÅsia, Åto ja nie vykarystoÅvaju tabulacyju.
Description[bg]=ÐÐÐÑÐÐÑÐÐ, ÑÐ ÐÐ ÑÐ ÐÐÐÐÐÐÑ ÑÐÐÑÐÐÑÐÐ.
Description[bn_IN]=àààààààààààà àààààà ààààà àààààààààà àààà ààààààà ààà ààà ààà
Description[ca]=Oblideu-vos que no utilitzeu les tabulacions.
Description[ca valencia]=Oblideu-vos que no utilitzeu les tabulacions.
Description[cs]=ZapomeÅte, Åe nepouÅÃvÃte tabulace.
Description[da]=Glem ikke at du bruger tabulatorer.
Description[de]=Vergessen Sie, dass Sie keine EinzÃge benutzen
Description[dz]=àààààààààà ààààààààààààààààààààààààààààààà àààààààà
Description[el]=ÎÎÎÎÏÎÏÎÎ ÎÎ-ÏÏÎÏÎÏ ÏÏÎÎÎÎÎÏÏÎ.
Description[en_CA]=Forget you're not using tabulations.
Description[en_GB]=Forget you're not using tabulations.
Description[es]=Olvidar que no està usando tabulaciones.
Description[eu]=Ahaztu tabulazioak ez zarela erabiltzen ari.
Description[fi]=Unohda, ettà et kÃytà sarkaimia.
Description[fr]=Oubliez que vous n'utilisez pas les tabulations.
Description[gl]=Esqueceu que vostede non està empregando tabulaciÃns.
Description[gu]=àààà ààà àà ààà ààààààààà ààààà ààààà ààà.
Description[he]=××××× ×××× ×× ××××× ××××××××.
Description[hu]=Felejtse el, hogy nem hasznÃl tabokat
Description[id]=Lupa bahwa Anda tidak menggunakan tabulasi.
Description[it]=Dimentica che non stai usando le tabulazioni.
Description[ja]=äèãäããããããããåããäããã
Description[kn]=àààà ààààààààààà àààààààààààààààà ààààààààààààà.
Description[lt]=PamirÅti, kad nenaudojate tabuliacijÅ.
Description[lv]=Aizmirstiet, ka neizmantojat tabulatorus.
Description[ml]=àààààààâ àààààààààâ ààààààààààààààààà ààààààà ààààààààààà.
Description[mr]=àààààà ààààà àààà ààà àààà àà àààààà àààà.
Description[nl]=Vergeet dat u geen tabs gebruikt.
Description[or]=ààààààààààà ààà ààààà ààààààà àààààààààààà
Description[pa]=àààà ààà àà ààààà ààààà àààà ààà ààà ààà
Description[pl]=Tabulacja nie jest juÅ potrzebna.
Description[pt]=Esquecer que nÃo està a utilizar tabuladores.
Description[pt_BR]=EsqueÃa que nÃo està usando tabulaÃÃes.
Description[ro]=UitÄ cÄ nu foloseÈti tabulatoare.
Description[ru]=ÐÐÐÑÐÑÑÐ Ð ÑÐÐ, ÑÑÐ ÐÑ ÐÐ ÐÐÐÑÐÑÐÑÐÑÑ ÑÐÐÑÐÑÑÐÐÐ.
Description[sk]=Zabudnite, Åe nepouÅÃvate tabulÃtory.
Description[sl]=Pozabi, da tabulatorji niso v uporabi.
Description[sr]=ÐÐÐÐÑÐÐÐÑÐ ÐÐ ÐÐ ÐÐÑÐÑÑÐÑÐ ÑÐÐÑÐÐÑÐÑÐ.
Description[sr latin]=Zaboravite da ne koristite tabulatore.
Description[sv]=GlÃm att du inte anvÃnder tabulatorer.
Description[ta]=ààààààà àààààà àààààààààà ààààààààààààààààà.
Description[te]=àààà àààààààààààààà àààààààààààà àààààààà ààààààà.
Description[th]=ààààààààààààààààààààààààààà
Description[vi]=QuÃn bán khÃng sá dáng cát Tab.
Description[zh_CN]=åèäçåèççåäã
Icon=gtk-unindent
Authors=Steve FrÃcinaux <steve istique net>, James Shubin <james shubin ca>
Copyright=Copyright  2006 Steve FrÃcinaux, Copyright  2013 James Shubin
Website=https://ttboj.wordpress.com/smarter-spaces/
Version=3.6.2
# -*- coding: utf-8 -*-

#  smarterspaces.py
#
#  Copyright (C) 2006 - Steve FrÃcinaux
#  Copyright (C) 2013 - James Shubin
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor,
#  Boston, MA 02110-1301, USA.

from gi.repository import GObject, Gtk, Gdk, GtkSource, Gedit
DEBUG = False                                                  # very useful
# TODO: check that the boilerplate and the loading/unloading details work right

class SmarterSpacesPlugin(GObject.Object, Gedit.ViewActivatable):
    __gtype_name__ = "SmarterSpacesPlugin"

    view = GObject.property(type=Gedit.View)

    def __init__(self):
        GObject.Object.__init__(self)

    def do_activate(self):
        self._handlers = [
            None,
            self.view.connect('notify::editable', self.on_notify),
            self.view.connect('notify::insert-spaces-instead-of-tabs', self.on_notify)
        ]

    def do_deactivate(self):
        for handler in self._handlers:
            if handler is not None:
                self.view.disconnect(handler)

    def update_active(self):
        # Don't activate the feature if the buffer isn't editable or if
        # we're using tabs
        active = self.view.get_editable() and \
                 self.view.get_insert_spaces_instead_of_tabs()

        if active and self._handlers[0] is None:
            self._handlers[0] = self.view.connect('key-press-event',
                                                   self.on_key_press_event)
        elif not active and self._handlers[0] is not None:
            self.view.disconnect(self._handlers[0])
            self._handlers[0] = None

    def on_notify(self, view, pspec):
        self.update_active()

    def get_real_indent_width(self):
        indent_width = self.view.get_indent_width()

        if indent_width < 0:
             indent_width = self.view.get_tab_width()

        return indent_width

    def on_key_press_event(self, view, event):
        # Only take care of backspace, shift+backspace, left, right
        mods = Gtk.accelerator_get_default_mod_mask()

        if DEBUG: print 'keyval: %s' % str(event.keyval)

        # FIXME: this condition is confusing as !
        bs_bool = event.keyval != Gdk.KEY_BackSpace or \
           event.state & mods != 0 and event.state & mods != Gdk.ModifierType.SHIFT_MASK

        # TODO: are there other variations of Left and Right that should be added here ?
        if event.keyval in [Gdk.KEY_Left, Gdk.KEY_Right]:
            return self.do_key_press_leftright(view, event, debug=DEBUG)

        elif not(bs_bool):
            return self.do_key_press_backspace(view, event)

        else:
            return False

    def do_key_press_backspace(self, view, event):
        mods = Gtk.accelerator_get_default_mod_mask()
        if event.keyval != Gdk.KEY_BackSpace or \
           event.state & mods != 0 and event.state & mods != Gdk.ModifierType.SHIFT_MASK:
            return False

        doc = view.get_buffer()
        if doc.get_has_selection():
            return False

        cur = doc.get_iter_at_mark(doc.get_insert())
        offset = cur.get_line_offset()

        if offset == 0:
            # We're at the begining of the line, so we can't obviously
            # unindent in this case
            return False

        start = cur.copy()
        prev = cur.copy()
        prev.backward_char()

        # If the previous chars are spaces, try to remove
        # them until the previous tab stop
        max_move = offset % self.get_real_indent_width()

        if max_move == 0:
            max_move = self.get_real_indent_width()

        moved = 0
        while moved < max_move and prev.get_char() == ' ':
            start.backward_char()
            moved += 1
            if not prev.backward_char():
                # we reached the start of the buffer
                break

        if moved == 0:
            # The iterator hasn't moved, it was not a space
            return False

        # Actually delete the spaces
        doc.begin_user_action()
        doc.delete(start, cur)
        doc.end_user_action()

        return True


    def do_key_press_leftright(self, view, event, debug=False):
        """Handle moving left and right over spaces as if they were tabs!
        Handle selection (shift) and control-selection (shift+control) too.
        Writing this function was off-by-one hell. Patch carefully and test
        extensively."""
        mods = Gtk.accelerator_get_default_mod_mask()

        if debug: print 'FUNCTION: do_key_press_leftright(keyval:%d)' % event.keyval

        both = bool(event.state & mods == Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.CONTROL_MASK)
        if debug: print 'BOTH: %s' % both

        if both: shift = True
        else: shift = bool(event.state & mods == Gdk.ModifierType.SHIFT_MASK)
        if debug: print 'SHIFT: %s' % shift

        if both: control = True
        else: control = bool(event.state & mods == Gdk.ModifierType.CONTROL_MASK)
        if debug: print 'CONTROL: %s' % control

        # TODO: are there other variations of Left and Right that should be added here ?
        if event.keyval != Gdk.KEY_Left and \
           event.keyval != Gdk.KEY_Right:
            return False

        doc = view.get_buffer()
        # NOTE: we don't do this because we want to work while under selection!
        #if doc.get_has_selection():
        #    return False

        tabwidth = self.get_real_indent_width()
        if debug: print 'TABWIDTH: %d' % tabwidth

        iterobj = doc.get_iter_at_mark(doc.get_insert())    # get cursor mark
        selecto = doc.get_iter_at_mark(doc.get_selection_bound())

        length = iterobj.get_chars_in_line()                # length of line
        if debug: print 'LENGTH: %d' % length

        offset = iterobj.get_line_offset()                  # an int
        if debug: print 'OFFSET: %d' % offset

        iter_a = doc.get_iter_at_mark(doc.get_insert())
        iter_a.set_line_offset(0)                           # move to start
        iter_b = doc.get_iter_at_mark(doc.get_insert())
        iter_b.forward_line()                               # move to end

        line_list = doc.get_slice(iter_a, iter_b, True)     # line as an array
        if length != len(line_list):                        # sanity check
            import inspect
            print '** (gedit:%s:%d): CRITICAL **: do_key_press_leftright: assertion `length == len(line_list)\' failed' % (__file__, inspect.currentframe().f_back.f_lineno)

        # if in the 'middle' of a tab, jump to edge
        lalign = ((tabwidth-offset) % tabwidth)
        ralign = (offset % tabwidth)

        if debug: print 'LALIGN: %d' % lalign
        if debug: print 'RALIGN: %d' % ralign

        space = True
        until = 0
        while until < len(line_list) and space:             # find continuous
            if line_list[until] != ' ':
                space = False
                break
            until = until + 1

        if debug: print 'UNTIL: %d' % until

        # XXX: does any of this need to be enclosed in "doc.begin_user_action()" and "doc.end_user_action()" ?
        motion = 1  # cursor moves itself by this (1)
        if event.keyval == Gdk.KEY_Left:

            if offset == 0:     # start of line
                return False

            if offset > until and line_list[offset-1] != ' ':  # not within continuous initial indentation
                return False

            if line_list[offset-1] == ' ':
                space = True
                luntil = offset
                while luntil > 0 and space:             # find continuous
                    if line_list[luntil-1] != ' ':
                        space = False
                        break
                    luntil = luntil - 1

                if debug: print 'LUNTIL: %d' % luntil
                iterobj.set_line_offset( max(offset - (tabwidth-lalign) + motion, luntil+1) )
            else:
                iterobj.set_line_offset( offset - (tabwidth-lalign) + motion )

        if event.keyval == Gdk.KEY_Right:

            if offset == length-1:  # end of line
                return False

            if offset > until-1 and line_list[offset+0] != ' ':     # not within continuous initial indentation
                return False

            if line_list[offset+0] == ' ':
                space = True
                runtil = offset
                while runtil < length and space:                # find continuous
                    if line_list[runtil+0] != ' ':
                        space = False
                        break
                    runtil = runtil + 1

                if debug: print 'RUNTIL: %d' % runtil
                iterobj.set_line_offset( min(offset + (tabwidth-ralign) - motion, runtil-1) )
            else:
                iterobj.set_line_offset( offset + (tabwidth-ralign) - motion )

        # do the placement
        doc.begin_user_action()
        if shift or both:   # as long as shift is being used...
            doc.select_range(iterobj, selecto)  # don't break the selection!
        else:
            doc.place_cursor(iterobj)
        doc.end_user_action()
        #return True    # XXX: why doesn't this work ?
        return False    # XXX: when this does!

# ex:ts=4:et:

Attachment: signature.asc
Description: This is a digitally signed message part



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]