Sublime Forum

Restoring ST2 cursor move by word behaviour in ST3 on OS X

#1

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).

1 Like

ST3 cursor word movement change
ST2 Word Navigation
word_separators question
#2

FWIW out of the box, ST3 caret movement is almost identical to ST2. (EDIT: not actually true, but the rest of this post is)

On OSX by default, option+arrow is bound to move by “word_ends”, rather than “words”. Using that should get you what you want.

If you need more control, then using the move command with “by”: “stops” should allow more configurability. When using stops, you need to tell the command which logical units to stop on. The options are:

"word_begin": false,
"word_end": false,
"punct_begin": false,
"punct_end": false,
"sub_word_begin": false,
"sub_word_end": false,
"line_begin": false,
"line_end": false,
"empty_line": false,

You’ll need to set at least one of the above to true.

0 Likes

#3

Brilliant, thanks for that info. I was able to get what I wanted. In detail…

Yes, but I guess as a typical software developer I’ve not used the out of the box movement defaults in any editor I’ve used for, I dunno, at least a couple of decades now. :wink:

I think I tried that, and it wasn’t what I wanted. I wanted to stop at word starts and ends. In ST2, move by word does that, in ST3 move by word skips over the ends and move by word_ends skips over starts. So, yes, moving by stops is exactly the flexibility I need.

[quote=“jps”]If you need more control, then using the move command with “by”: “stops” should allow more configurability. When using stops, you need to tell the command which logical units to stop on. The options are:

"word_begin": false,
"word_end": false,
"punct_begin": false,
"punct_end": false,
"sub_word_begin": false,
"sub_word_end": false,
"line_begin": false,
"line_end": false,
"empty_line": false,

You’ll need to set at least one of the above to true.[/quote]

Aha! “line_begin” and “line_end” looks like what I was missing in my previous attempts. I didn’t see them mentioned in any of the documentation I had found so far.

For anyone who is interested, to replicate ST2’s move by word behaviour in ST3, you’ll need the following arguments to the move command:

"by": "stops", "word_begin": true, "word_end": false, "line_begin": true, "line_end": true, "punct_begin": true, "punct_end": false

1 Like