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