Here’s a simple plugin - extend_selection.py - that allows you to use the cursor movement keys to expand the area of selected text without the need to simultaneously hold down the shift key. Create a keybinding for the “toggle_extend_mode” command. For example,{ “keys”: [“ctrl+space”], “command”: “toggle_extend_mode”}. Cancel extend selection mode by pressing the toggle key again. Extend selection mode is also canceled when you type something over selected text, copy the selection, or delete it.
myflag = 1
import sublime, sublime_plugin
class toggle_extend_mode(sublime_plugin.TextCommand): #Toggle extend_selection on or off
def run(self, edit):
global myflag
if myflag == 1:
self.view.set_status('extstatus', "<EXTEND_SELECTION>")
myflag = 0
else:
self.view.set_status('extstatus', "")
myflag = 1
class KeyboardListener(sublime_plugin.EventListener): #If extend mode is on, then typing over a selection
def on_modified(self, view): #will turn it off
global myflag
if myflag == 0:
myflag = 1
view.set_status('extstatus', "")
class TextCommandEventListener(sublime_plugin.EventListener): #Inject extend:true into move and move_to commands when extend_mode is on.
def on_text_command(self, view, command, args): #Note: Using return command here to change the cursor move parameters will
global myflag #cause this Listener to be re-entered. Filter out extend:true to prevent command
if myflag == 0 : #doubling. Thanks to kingkeith from the sublime text forum for figuring this out!
if command == "move" and ("extend" not in args or args["extend"] == False): #move command, extend it.
return "move", {'forward' : args['forward'], 'by': args['by'], 'extend': True}
elif command == "move_to" and ("extend" not in args or args["extend"] == False) : #move_to command, extend it
return "move_to", {'extend' : True, 'to' : args['to']}
elif command == "left_delete" or command == "right_delete" or command == "copy" : #copy/delete selected text -> extend_selection off
myflag = 1
view.set_status('extstatus', "")
Just a quick look in the code, first, when you put a code block, do it this way
```python
import this # my python code
echo "i'm a really messy langage"
so that your code is highlited (working on plenty of website, github for example)
Then, you use `global`, which is not really good, and you should use a different name than `myflag`, it's like `temp`, or `youdontknowwhatthisis` :smile:
Why do you have to class `EventListners` ? You can put them in the same class, and you should choose a more specific name (so you don't get any conflict with any other plugin)
Also, you should indent your code using 4 spaces (pep 8 I think). Your class name should be in camel case (`MyClassName`), unlike variables and functions/methods who should be in snake case I think it's called (`my_function_name`)
That's a lot of "criticts", but I hope you'll take them the right way, and don't get upset about it :worried:
So, really, have a look at vim. Maybe you're already using it...
Matt
Thanks! I don’t mind the criticism at all, since I am a rank amateur and your feedback is really helpful. I understand most of what you are saying. I will put the two EventListeners into the same class. I also get that globals are not looked upon kindly, though at my level of skill it is the easiest way to get the EventListeners to access variables in another class. (I do understand though that it is bad to use ‘myflag’ since that name is like ‘temp’ and could be used anywhere.) I will also make the formatting changes you suggest.
In regard to vim, I’ve used vim before, but I’m not sure I want to use the full vintage mode just to get the ability to toggle extend_selection. Anyway, I will make the changes you suggest - and try to figure out an alternative to those global statements - and then I’ll post up the revision. Thanks again!
So here’s a revision of extend_selection.py. I’ve incorporated the changes suggested by Matt: class variables instead of global variables and the two EventListeners are in a single class. I also made an attempt to follow the naming convention he suggested. (ClassNamesLikeThis and function_names_like_this.) I couldn’t get my keybinding to work reliably with a camel case command name however, so I had to settle for snake case on the first class name. (I.e., “class toggle_extend_mode.”) As for vim: I think Matt’s point is that vim does everything this plugin does and more. If you are using vim you would have no need for this plugin, since you already have visual mode. I agree of course, but vim is a lot more than visual mode - and I’m not looking to add that extra functionality here. (I think this is what Matt was suggesting.) This plugin is just meant to make it a bit easier and quicker to select text with the direction/cursor movement keys. (I’ve also tried to follow Matt’s suggestions about formatting the code that I post here. I’ve indented 4 spaces, but I’m not sure I understand the point about “highlighted” code.)
import sublime, sublime_plugin
class toggle_extend_mode(sublime_plugin.TextCommand):
extend_status_flag = 2 #0 = extend on, 1 = extend off
def run(self, edit):
if toggle_extend_mode.extend_status_flag != 0 :
self.view.set_status('extstatus', "<EXTEND_SELECTION>")
toggle_extend_mode.extend_status_flag = 0
else :
self.view.set_status('extstatus', "")
toggle_extend_mode.extend_status_flag = 1
class ExtendSelectionToggleEventCapture(sublime_plugin.EventListener):
def on_modified_async(self, view): #New text cancels extend
if toggle_extend_mode.extend_status_flag == 0:
view.set_status('extstatus', "")
toggle_extend_mode.extend_status_flag = 1
def on_text_command(self, view, command, args):
if toggle_extend_mode.extend_status_flag == 0 :
if command == "move" and ("extend" not in args or args["extend"] == False) :
return "move", {'forward' : args['forward'], 'by': args['by'], 'extend': True}
elif command == "move_to" and ("extend" not in args or args["extend"] == False) :
return "move_to", {'extend' : True, 'to' : args['to']}
elif command == "left_delete" or command == "right_delete" or command == "copy" :
view.set_status('extstatus', "")
toggle_extend_mode.extend_status_flag = 1
Specifically in regards to indenting 4 spaces, he was referring to PEP 8, and in particular the section on indentation, which recommends 4 spaces per indent level (you’re using 2).
The highlighting part has to do with syntax highlighting when you post code on the forum, I think. If you just indent your code 4 spaces when you post it (or select it and click the Preformatted Text button), it comes out like:
class toggle_extend_mode(sublime_plugin.TextCommand):
extend_status_flag = 2 #0 = extend on, 1 = extend off
def run(self, edit):
if toggle_extend_mode.extend_status_flag != 0 :
self.view.set_status('extstatus', "<EXTEND_SELECTION>")
toggle_extend_mode.extend_status_flag = 0
else :
self.view.set_status('extstatus', "")
toggle_extend_mode.extend_status_flag = 1
If you wrap it like:
```python
my code here
```
It gets syntax highlighted, making it easier to read. In this case you don’t have to indent it (or use that Preformatted text button), which also makes it handier.
class toggle_extend_mode(sublime_plugin.TextCommand):
extend_status_flag = 2 #0 = extend on, 1 = extend off
def run(self, edit):
if toggle_extend_mode.extend_status_flag != 0 :
self.view.set_status('extstatus', "<EXTEND_SELECTION>")
toggle_extend_mode.extend_status_flag = 0
else :
self.view.set_status('extstatus', "")
toggle_extend_mode.extend_status_flag = 1
Can you explain how exactly it wasn’t working reliably? Internally Sublime converts from CamelCase to snake_case, so if your command was named ToggleExtendModeCommand it would be converted by sublime to toggle_extend_mode. Possibly you were using the wrong command name in your binding?
Thanks for explaining the indenting and highlighting - now I get it! As for the keybind, it looks like I did that wrong too!! I had:
{ “keys”: [“ctrl+space”], “command”: “ToggleExtendMode”},
But “ToggleExtendMode” is supposed to be “ToggleExtendModeCommand”?
class ToggleExtendModeCommand(sublime_plugin.TextCommand):
def run(self, edit):
pass
gets converted to the command name toggle_extended_mode. Basically it throws away the Command on the end and then converts all uppercase characters to lowercase and prefixes them with an underscore (except the first one). I believe that the Command part is optional and the conversion will still work, but it’s considered good coding practice to use the Command suffix on the class name.
is for the cool kids, == for the rest [convention]
When you compare a value to a boolean (True, False) or None, use the operator is instead of ==, and is not instead of !=.
Your flag
You said it yourself, this is a flag, and as you see, it has only 2 possible values (1, or 0). So, you could use some booleans.
Then your name can be improved. You put a comment, but always remember
Perfect developers don’t need to write comments
It doesn’t mean that we shouldn’t write comments, but you should avoid it.
Good code writing workflow (at least what I consider being a good one)
think, sketch on a bit of paper if needed
write as clearly as possible (Programs must be written for people to read, and only incidentally for machines to execute. – Harold Abelson)
if it’s not clear enough, then write comment
So, say your flag name would be is_extend_selection_active, or extending_selection? It would speak for itself, wouldn’t it? (if you’re using boolean, it would even more). So in this case, you don’t need comment.
import should be on several line (with a secret sublime tip ) [convention]
When you use import, you should write one import for each “library” you import, instead of using commas. Like so
More typing? Yes, but it’s cleaner, and in this case, I’m going to give you a tip so that you have to press even less keys! But it secret, so shhh ( just kidding)
Type import sublime, press ctrl+shift+d (on windows, otherwise, go have a look at what it is here: Edit -> Line -> Duplicate Line) and now, you just have to type _plugin.
When you do this: command == "left_delete" or command == "right_delete" or command == "copy". Picture this: you have 100 different possibilities. What would you do. Copy/paste? Here’s the good solution.
if command in ['left_delete', 'right_delete', 'copy']:
# do your stuff
Comments
I’m not sure if it’s a convention, but it looks for me cleaner when you add a space after your #.
#no
#stin?ky comment
# yes
# Beautiful is better than ugly.
It’s up to you to do this, but this part is more an excuse to give you an other tip (maybe you already knew that one): ctrl+/ (un)comments the current line if there is not selection, otherwise, it toggles the selection (try it).
Colons
There shouldn’t be any space before a :.
#no
if something == True :
print ("something's wrong, I can feel it -- Eminem")
# yes
if something is True:
print("a good looking code")
I don’t think you understood what I meant. Look:
# indent code using **2** spaces
class MySuperCommandCommand(sublime.TextCommand):
def run(self, edit):
print('You noticed my command name. It can be called from key binding for example like this my_super_command')
print('As @OdatNurd explained, the `Command` at the end is automatically striped')
print("Here, I didn't have a choice about adding the conventional `Command`")
print(", because I would have had to call my command `my_super` otherwise")
print("If you have any trouble with this, I've a made a plugin that allows you to transform you command name")
print("easily: https://github.com/math2001/st-user-package/blob/master/TransformCommandName.py")
# indent code using **4** spaces
class MySuperCommandCommand(sublime.TextCommand):
def run(self, edit):
print('You noticed my command name. It can be called from key binding for example like this my_super_command')
print('As @OdatNurd explained, the `Command` at the end is automatically striped')
print("Here, I didn't have a choice about adding the conventional `Command`")
print(", because I would have had to call my command `my_super` otherwise")
print("If you have any trouble with this, I've a made a plugin that allows you to transform you command name")
print("easily: https://github.com/math2001/st-user-package/blob/master/TransformCommandName.py")
After this, your code should look pythonic (awesome )
TL;DR: please read it
BTW: open the sublime console, and type: import this. It’s the 20 aphorisms guiding principles for Python’s design. Read them, and count them () a bit more info
So, I have a structurational suggestion: if you assign a setting value to a view, the potential inconsistencies of sharing the same flag amongst all views is easily worked around and you could even directly define key bindings with a context specification instead of implementing an event listener. You would do it like this: view.settings().set("selection_mode", True).
I would link more references but I cba when on mobile.
Matt, OdatNurd and FictheFoll: Thank you for your feedback - it is very helpful.
OdatNurd: It seems I needed to restart sublime to get my new keybindings to take. Failing to do that is apparently why changing the command name to CamelCase didn’t work with my new keybinding.
OdatNurd and Matt: I think I really do finally get how to format the code blocks with proper indents and for posting here. I don’t know if it is as vivid of Dorothy’s arrival in the Land of Oz, but I see the colors now.
Matt: “TL;DR: please read it.” Since you took the time to write it, the least I can do is read it! Good point that a truly descriptive name for a boolean flag makes the meaning of the code clearer with much less need for a comment. I think I worry that being too spare with comments introduces undesirable ambiguity, but I gather the ‘pythonic’ way is make the code better and more clear to begin with. Also, thanks for the suggestions about white space, colons, import statements, and “in”. The Zen of Python and the concept of “pythonic” were new to me. The point then is that it isn’t just about making the code “work”, or even listening to the accumulated experience of a bunch of programmers recommending the most efficient way of doing things. In addition, a community (with its own aesthetic, or even ‘culture’) develops around the concept of a “proper way of doing things.” Asserting and maintaining that idea helps sustain this community. In a broader world where the “right way of doing things” seems to be increasingly threatened, it is reassuring to find this idea alive and well here!
FichteFoll: I do see how a setting value could replace the flag and how keybindings could replace the on_text_command EventListener. I don’t understand though how to avoid the need for the on_modified EventListener. If I want to cancel selection_mode when the user enters new text over a selection, do I not need on_modified? Maybe though I am not understanding your suggestion?
import sublime
import sublime_plugin
class ToggleExtendMode(sublime_plugin.TextCommand):
extend_selection_mode_on = False
def run(self, edit):
if not ToggleExtendMode.extend_selection_mode_on:
self.view.set_status('extend_mode_status_bar_msg', "<EXTEND_SELECTION>")
ToggleExtendMode.extend_selection_mode_on = True
else:
self.view.set_status('extend_mode_status_bar_msg', "")
ToggleExtendMode.extend_selection_mode_on = False
class ExtendSelectionToggleEventCapture(sublime_plugin.EventListener):
def on_modified(self, view): # Typing over a selection cancels extend
if ToggleExtendMode.extend_selection_mode_on:
view.set_status('extend_mode_status_bar_msg', "")
ToggleExtendMode.extend_selection_mode_on = False
def on_text_command(self, view, command, args):
if ToggleExtendMode.extend_selection_mode_on:
if command == "move" and ("extend" not in args or args["extend"] is False):
return "move", {'forward' : args['forward'], 'by': args['by'], 'extend': True}
elif command == "move_to" and ("extend" not in args or args["extend"] is False):
return "move_to", {'extend' : True, 'to' : args['to']}
elif command in ["left_delete", "right_delete", "copy"]:
view.set_status('extend_mode_status_bar_msg', "")
ToggleExtendMode.extend_selection_mode_on = False
Ah sorry, I specifically meant the on_text_command hook with that. With the setting set, you would then define key keybindings that are only active while that setting’s value is true. This is also how the Vintage package works.