In Word’s advanced options, there’s an option called “Typing Replaces Selected Text” this is enabled by default, but I have a scenario where it needs to be disabled. Is there any way to duplicate this functionality in sublime?
Disable "Typing Replaces Selected Text" is there a way?
I’m not much of a word user, but a bit of quick googling seems to indicate that when this option is turned off and there is selected text, the selection is removed and then the appropriate text is inserted. I’m not aware of any native option in Sublime that would do something like that, but Sublime is flexible enough to allow you to accomplish that goal if you want to (or something close to it, see below).
What needs to happen is that when you press a key to type a character, if there is any selected text the selection needs to be removed, leaving the caret where it is, followed by inserting the character. To that end a couple of new Sublime commands are needed, which we can implement as follows by selecting Tools > Developer > New Plugin...
and replacing the stub code with the following, then saving in the default location Sublime will offer with a memorable name like unselect_and_insert.py
:
import sublime
import sublime_plugin
class EmptySelectionsCommand(sublime_plugin.TextCommand):
"""
Empty all selections, leaving the caret for each selection in place.
"""
def run(self, edit):
selections = [sel.b for sel in self.view.sel()]
self.view.sel().clear()
for sel in selections:
self.view.sel().add(sublime.Region(sel))
class UnselectAndInsertCommand(sublime_plugin.TextCommand):
"""
Insert the given character at all caret locations after removing the
selection first.
"""
def run(self, edit, character):
self.view.run_command("empty_selections")
self.view.run_command("insert", {"characters": character})
The first of these commands, empty_selections
, operates over all of the selections that exist and sets them to be a length of 0; the caret or carets are left where they are before the operation, but the text that used to be selected no longer is.
The second command takes an argument of a character, then empties the selections and uses the internal insert
command to insert the character that you gave it.
Next, presumably this is an option that you want to be able to enable or disable as needed; if nothing else it is extremely disconcerting to try to type over the selected text and have it unselect the text first when you don’t expect it. To that end, use Preferences > Settings
to open your preferences and add the following:
// When set to true, entering text while there is a selection will remove
// the selection first.
"unselect_on_input": true,
The setting can also be used in a sublime-project
file if you want to only enable it for certain projects or in the syntax specific settings of a file type to have it only be enabled in certain types of files.
With these two parts in place, we can tie everything together by adding the following key binding to your user key bindings in Preferences > Key Bindings
:
{
"keys": ["<character>"],
"command": "unselect_and_insert",
"context": [
{ "key": "selection_empty", "operator": "equal", "operand": false, "match_all": false },
{ "key": "setting.unselect_on_input", "operator": "equal", "operand": true },
{ "key": "setting.is_widget", "operator": "equal", "operand": false },
],
},
This binding will trigger on any character that would normally cause input in the buffer and invoke our unselect_and_insert
command with an argument named character
that represents what would have been inserted. The context
here makes this key binding apply only when there is something selected at the same time as our new setting is turned on.
Taken all together, no matter how many carets there are in the buffer or how many of them have any selected text, as long as there’s at least one caret with a non-empty selection and you press a key to type something, the selection will be removed and the character inserted as normal, but only when the setting is turned on.
I’m not familiar with all of the subtleties of this particular feature of word, so something to note is that this only triggers when you press a character that would type something.
In particular that means:
- If you paste while there is selection, the selection will still be clobbered away.
- If you press Backspace while there is a selection, the selection will still be deleted
- If a plugin takes an action to insert text on your behalf, this will have no effect
- Probably other cases I’m not thinking of at the moment
If needed you can solve the first couple of these by creating alternate key bindings for pasting and backspacing that execute a macro that first runs empty_selection
and then the appropriate command (e.g. paste
or left_delete
); that’s why the plugin above implements this as two commands instead of blending the functionality into a single command.
That looks like it might do the trick, I’ll test it out tomorrow—hopefully. The other case that I’d be concerned about is what it does when you execute a custom key-binding that wraps the selection with some sort of code, like the default tag wrapping shortcut: Ctrl-Shift-W.
In the case of that particular command, you get a “sort of works, sort of doesn’t” scenario.
As defined above, the key binding that makes this happen only triggers if you press a key that literally types a character, so using that key binding will still wrap the selected text with a tag like you want, because what you’re pressing to trigger it isn’t typing something directly, which falls into the a plugin takes an action to insert text on your behalf
scenario.
On the other hand, once that’s done, you’re left with the ability to type the sort of tag that you want to wrap the text with. This is accomplished by a snippet that includes a mirrored field that mimics what you type in the first field (i.e. the open tag).
That field has a default value of p
to wrap in a paragraph tag, and it’s selected so that you can easily change it. Say you want to wrap your text in <div>
instead, so you press d
, and now you’re pressing a key that directly inserts a character while there’s a selection, and things go off the rails.
One solution to that problem is to press Backspace (or Delete on MacOS, which it looks like you’re using) to delete the default text, which also removes the selection.
Another solution would be to modify the key binding above to the following one instead:
{
"keys": ["<character>"],
"command": "unselect_and_insert",
"context": [
{ "key": "selection_empty", "operator": "equal", "operand": false, "match_all": false },
{ "key": "has_next_field", "operator": "equal", "operand": false },
{ "key": "setting.unselect_on_input", "operator": "equal", "operand": true },
{ "key": "setting.is_widget", "operator": "equal", "operand": false },
],
},
This is the same as the above but with an extra context item that requires that there not be a next snippet field available, which will stop the binding from taking effect in this situation so that you can enter the tag as per normal.
Note that I made an edit to the key binding here and also in the original post to disallow the binding while the is_widget
setting is turned on. This setting is applied automatically by Sublime for text widgets where you’re entering input (such as the command palette), so including this setting here will still allow you to overwrite selected text in those situations.