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