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



Updated version!

Top posting, because it was me before...
FYI, there's now an updated version of the code that now has all the
other things everybody asked for:
https://bugzilla.gnome.org/show_bug.cgi?id=693283

I'm attaching the files here, but it's yucky, so probably best to get
them off bugzilla instead.

Thanks to gregier who helped me find my way around plugin_info. He now
gets co-authorship for his wisdom.

James

On Tue, 2013-02-05 at 00:31 -0500, James wrote:
> 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
> 

Attachment: org.gnome.gedit.plugins.smarterspaces.gschema.xml
Description: application/xml

[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>;Garrett Regier <garrettregier gmail com>;James Shubin <james shubin ca>
Copyright=Copyright  2006 Steve FrÃcinaux;Copyright  2013 Garrett Regier;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 - Garrett Regier
#  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, PeasGtk, Gio
DEBUG = False                                                  # very!! useful
SCHEMA_ID = 'org.gnome.gedit.plugins.smarterspaces'
# TODO: check that the boilerplate and the loading/unloading details work right

class SmarterSpacesPluginSettings(GObject.Object, PeasGtk.Configurable):

    def do_create_configure_widget(self):
        settings = self.plugin_info.get_settings(SCHEMA_ID)

        # TODO: this sticks out on display, and doesn't wrap...
        label = Gtk.Label('These settings let you remove, delete and move '\
        'through space indented code, as if it was using tabs.')
        check1 = Gtk.CheckButton('Enable smart backspace.')
        check2 = Gtk.CheckButton('Enable smart delete.')
        check3 = Gtk.CheckButton('Enable smart arrows.')
        check4 = Gtk.CheckButton('Enable smart keypad arrows.')

        settings.bind('smart-backspace', check1, 'active', Gio.SettingsBindFlags.DEFAULT)
        settings.bind('smart-delete', check2, 'active', Gio.SettingsBindFlags.DEFAULT)
        settings.bind('smart-arrows', check3, 'active', Gio.SettingsBindFlags.DEFAULT)
        settings.bind('smart-kparrows', check4, 'active', Gio.SettingsBindFlags.DEFAULT)

        vbox = Gtk.VBox(False, 5)
        vbox.add(label)
        vbox.add(check1)
        vbox.add(check2)
        vbox.add(check3)
        vbox.add(check4)

        return vbox

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):
        # the magic plugin_info attribute comes from here:
        # http://git.gnome.org/browse/libpeas/tree/loaders/python/peas-plugin-loader-python.c#n177
        self.settings = self.plugin_info.get_settings(SCHEMA_ID)

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

        self.reconfigure()

    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 reconfigure(self):
        # load any settings you want here...
        pass

    def on_settings_changed(self, settings, key):
        self.reconfigure()

    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, delete...
        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

        dl_bool = event.keyval != Gdk.KEY_Delete or \
        event.state & mods != 0 and event.state & mods != Gdk.ModifierType.SHIFT_MASK

        if event.keyval in [Gdk.KEY_Left, Gdk.KEY_Right] and self.settings.get_boolean('smart-arrows'):
            return self.do_key_press_leftright(view, event, debug=DEBUG)

        elif event.keyval in [Gdk.KEY_KP_Left, Gdk.KEY_KP_Right] and self.settings.get_boolean('smart-kparrows'):
            return self.do_key_press_leftright(view, event, debug=DEBUG)

        elif not(bs_bool) and self.settings.get_boolean('smart-backspace'):
            return self.do_key_press_backspace(view, event)

        elif not(dl_bool) and self.settings.get_boolean('smart-delete'):
            return self.do_key_press_delete(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

    # NOTE: this algorithm isn't perfect, but will probably do the job. Ping me
    # if you find any corner cases that you'd like to be handled differently.
    def do_key_press_delete(self, view, event):
        mods = Gtk.accelerator_get_default_mod_mask()
        if event.keyval != Gdk.KEY_Delete 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()
        length = cur.get_chars_in_line()                # length of line

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

        start = cur.copy()
        prev = cur.copy()
        #prev.forward_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.forward_char()
            moved += 1
            if not prev.forward_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

        if event.keyval != Gdk.KEY_Left and \
        event.keyval != Gdk.KEY_Right and \
        event.keyval != Gdk.KEY_KP_Left and \
        event.keyval != Gdk.KEY_KP_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

        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
        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)
        #return True    # TODO: shouldn't this work ?
        return False    # TODO: however 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]