My preferred mode of cursor navigation is to move left and right by word. I may very well be missing something obvious, but the behaviour of the move command with a “by word” argument appears to have changed between ST 2 and ST 3, at least on OS X. In ST 2 the cursor would move to the first character in the word, then the next move command would move it to the first non-word character, then the next move command would move it to the first word character in the next word. At the last word or sequence of non-word characters in the line, the next move command would move the cursor to the end of the line, then the next move command would move the cursor to the beginning of the next line. In ST3, move by word is completely different: each move by word command appears to only move the cursor to the beginning of the next word, including skipping over line ends.
I’ve managed to somewhat replicate the basic move by word behaviour from ST2 in ST3 via a series of 4 context-specific key bindings. Moving by “stops” (whatever they are; I have not found any documentation defining what a “stop” is) does most of what I want, except the behaviour near the end of the line, where it merrily skips over the last word or non-word and past the end of the line to the first non-whitespace character in the next line. Hence the need for a pile of special-case key bindings (OK, 3 additional ones to override the default move by stops).
“What’s my problem?”, you may be asking by this point. Well, it strikes me that such a basic cursor movement pattern should be more obvious to implement, so I want to know what I’ve missed. But more importantly, my technique seems to fail completely with selecting text. And that’s exactly when I’d like to have the extra precision in cursor movement. Unfortunately, it looks like just copying the navigation key bindings I came up with, binding them to the Shift versions of the keys and adding an “extend” argument doesn’t seem to result in the same contexts being pattern-matched. This is bad, because I often want to select a keyword or identifier at the end of the line, and not all the following whitespace onto the next line. And no, I don’t like navigating by subwords, mainly because I mostly develop in Objective-C and with all the camel-casing there it involves far too many micro movements.
So, here are my key bindings (yes I know that they aren’t typical OS X defaults, I’ve just used these on many platforms over the years, and no they are not bound to anything in OS X, the key strokes are available to ST3):
// Need a somewhat convoluted set of rules for navigating to the right because, by default, Sublime Text 3 skips over
// far too many characters (except for navigating by subwords) and simply navigating by stops gets closer to the v2
// behaviour but still skips over the end of the line and to the next non-whitespace when in or before the last token
// on the line. We end up needing four rules, one unconditional and the others with a context near or at the EOL.
// Note that for context-specific rules #2 amd #3, there is no reason for the "by" arguments to be different: since
// these all skip over a final set of word/non-word characters, each mode of movement will skip to the EOL. I'm just
// using different modes to more easily debug which command is firing when logging via sublime.log_commands(True).
// 1. By default, navigate by "stops". This rule applies if none of the later contexts are true.
{ "keys": "ctrl+right"], "command": "move", "args": {"by": "stops", "forward": true, "word_begin": true, "punct_begin": true, "separators": false} },
// 2. If only non-word characters lie between the cursor and EOL, skip to EOL.
{ "keys": "ctrl+right"], "command": "move", "args": {"by": "subwords", "forward": true}, "context":
{ "key": "following_text", "operator": "regex_match", "operand": "^a-zA-Z0-9_]+$", "match_all": false },
]
},
// 3. If only word characters lie between the cursor and EOL, skip to EOL.
{ "keys": "ctrl+right"], "command": "move", "args": {"by": "words", "forward": true}, "context":
{ "key": "following_text", "operator": "regex_match", "operand": "[a-zA-Z0-9_]+$", "match_all": false },
]
},
// 4. If the cursor is at the end of the line, skip to the beginning of the next line.
{ "keys": "ctrl+right"], "command": "move", "args": {"by": "characters", "forward": true}, "context":
{ "key": "following_text", "operator": "regex_match", "operand": "^$", "match_all": false },
]
},
{ "keys": "ctrl+shift+right"], "command": "move", "args": {"by": "stops", "forward": true, "word_begin": true, "punct_begin": true, "separators": true, "extend": true} },
// The following context-specific rules don't seem to work when extending a selection, they only seem to work when
// starting a selection.
{ "keys": "ctrl+right"], "command": "move", "args": {"by": "subwords", "forward": true, "extend": true}, "context":
{ "key": "following_text", "operator": "regex_match", "operand": "^a-zA-Z0-9_]+$", "match_all": false },
]
},
{ "keys": "ctrl+shift+right"], "command": "move", "args": {"by": "words", "forward": true, "extend": true}, "context":
{ "key": "following_text", "operator": "regex_match", "operand": "[a-zA-Z0-9_]+$", "match_all": false },
]
},
{ "keys": "ctrl+shift+right"], "command": "move", "args": {"by": "characters", "forward": true, "extend": true}, "context":
{ "key": "following_text", "operator": "regex_match", "operand": "^$", "match_all": true },
]
},
What am I doing wrong in the selection extending case? Is there actually a bug in the selection extending code that prevents the correct command contexts from matching?
Basically, what I’d really like is for moving by word to be more closely related to moving by subword (moving by subword doesn’t skip over punctuation and whitespace to the beginning of the next subword).