The past few weeks I've gotten into customizing my Mathematica keybindings so I figured I'd share them and share what I've learned. This all works as stated on my system ($Version
is 12.1.1 for Linux x86 (64-bit) (June 19, 2020)
) but I have not tested these elsewhere so YMMV.
If you like to use input aliases and find hitting Esc
twice inconvienent then add this to your init.m
file located in FileNameJoin[{$UserBaseDirectory, "Kernel"}]
.
SetOptions[SelectedNotebook[],
NotebookEventActions -> {{"KeyDown", "\t"} :> KeyBindings`OnTab[]}];
KeyBindings`OnTab[] := Module[{nb, type, range},
nb = SelectedNotebook[];
{type, range} = {"CellSelectionType", "CharacterRange"} /. FrontEndExecute @ FrontEnd`UndocumentedGetSelectionPacket[nb];
If[type === "ContentSelection",
FrontEndExecute @ FrontEndToken[nb, "Tab"];
If[range === ("CharacterRange" /. FrontEndExecute @ FrontEnd`UndocumentedGetSelectionPacket[nb]),
(* If hitting tab does nothing then its not for navigation or code completion so we are good to run our code *)
If[range[[1]] === range[[2]],
FrontEndTokenExecute[nb, "ExpandSelection"]
];
NotebookApply[nb, "\\[AliasDelimiter]\\[SelectionPlaceholder]\\[AliasDelimiter]"]
]
]
];
Now if you type an alias and hit tab it should autoexpand appropriately, no Esc
needed.
For the rest of these you'll need to actively modify your KeyEventTranslations.tr
and MenuSetup.tr
files. This post is a good collection of links for the myriad of ways people have come up with to do this (its also quite the read).
My advice: find the originals on your system, for me they are at FileNameJoin[{$InstallationDirectory, "12.1", "SystemFiles", "FrontEnd", "TextResources", "X"}]
the X
will change depending on your OS, copy only KeyEventTranslations.tr
and MenuSetup.tr
to FileNameJoin[{$UserBaseDirectory, "SystemFiles", "FrontEnd", "TextResources", "X"}]
(once again the X
is OS dependent, you may need to make these directories if they do not already exist). Mathematica gives these files priority over the ones in $InstallationDirectory
, you should make changes here so you always have clean copies on your system to default to.
KeyEventTranslations.tr
lets you set keybindings, MenuSetup.tr
lets you both set keybindings and make additional menus and menu items. Keybindings in MenuSetup.tr
have priority over those in KeyEventTranslations.tr
. When making a new keybinding, you must comment out other bindings (in both files) that use that key combo, e.g. if you want to attach something to Ctrl + c you must find the lines of code that make Ctrl + c do Copy
and comment them out.
Often you must surround some code in brackets. I set bindings for [
to surround the selection (the highlighted code) in these. To do this add this to KeyEventTranslations.tr
Item[
KeyEvent["[", Modifiers -> {}],
KernelExecute @ KeyBindings`OnOpenBracket[],
MenuEvaluator -> Automatic
]
and this to your init.m
KeyBindings`OnOpenBracket[] := (
Module[{type, range},
{type, range} = {"CellSelectionType", "CharacterRange"} /.
FrontEndExecute @ FrontEnd`UndocumentedGetSelectionPacket[SelectedNotebook[]];
If[type === "ContentSelection" && range[[1]] =!= range[[2]],
NotebookApply[SelectedNotebook[], "[\[SelectionPlaceholder]]", All],
NotebookWrite[SelectedNotebook[], "["]]
];
);
it is easy to make these for {
, (
, and <
as well.
Often you want to do the opposite: to desurround the selection. This is a bit harder. Add this to KeyEventTranslations.tr
Item[
KeyEvent["Backspace", Modifiers -> {Control, Shift}],
KernelExecute @ KeyBindings`Desurround[],
MenuEvaluator -> Automatic
]
And this to init.m
(* Gets the first character of a box structure, if the first thing is
not a character, returns None *)
KeyBindings`FirstCharacter[string_String] := First@Characters@string;
KeyBindings`FirstCharacter[RowBox[{first_, ___}]] :=
KeyBindings`FirstCharacter@first;
KeyBindings`FirstCharacter[_] := None;
(* Gets the last character of a box structure, if the last thing is
not a character, returns None *)
KeyBindings`LastCharacter[string_String] := Last@Characters@string;
KeyBindings`LastCharacter[RowBox[{___, last_}]] :=
KeyBindings`LastCharacter@last;
KeyBindings`LastCharacter[_] := None;
KeyBindings`Desurround[] := Module[{nb, selection, range, first, last},
nb = SelectedNotebook[];
selection = NotebookRead[nb];
range =
"CharacterRange" /.
FrontEndExecute@FrontEnd`UndocumentedGetSelectionPacket[nb];
first = KeyBindings`FirstCharacter@selection;
last = KeyBindings`LastCharacter@selection;
If[(first === "[" && last === "]") ||
(first === "{" && last === "}") ||
(first === "(" && last === ")") ||
(first === "\"" && last === "\"") ||
(first === "<" && last === ">") ||
(first === "|" && last === "|") ||
(first === "\[LeftAssociation]" && last === "\[RightAssociation]"),
FrontEndExecute@FrontEndToken[SelectedNotebook[], "MoveNext"];
FrontEndExecute@FrontEndToken[SelectedNotebook[], "DeletePrevious"];
SelectionMove[nb, Previous, Character, range[[2]] - range[[1]] - 1];
FrontEndExecute@FrontEndToken[SelectedNotebook[], "DeleteNext"];
SelectionMove[nb, All, Character, range[[2]] - range[[1]] - 2];
];
If[first === "<" && last === ">",
KeyBindings`Desurround[]
];
];
Theres actually a FrontEndToken, "SelectionUnbracket"
, that does this, but it doesn't work under many circumstances.
Often I want to delete the cell I am in, or, if between cells, the cells above the cursor. Add this to KeyEventTranslations.tr
Item[
KeyEvent["r", Modifiers -> {Control}],
KernelExecute @ KeyBindings`DeleteCell[],
MenuEvaluator -> Automatic
]
And this to init.m
KeyBindings`DeleteCell[] := Module[{nb},
nb = SelectedNotebook[];
Switch["CellSelectionType" /. FrontEndExecute @ FrontEnd`UndocumentedGetSelectionPacket[nb],
"ContentSelection",
SelectionMove[nb, All, Cell];
NotebookDelete[nb],
"BelowCell" | "AboveCell",
KeyBindings`SelectUpCell[];
NotebookDelete[nb],
"OnCell" | "CellRangeSelection",
NotebookDelete[nb]
]
];
KeyBindings`SelectUpCell[] := Module[{nb, content, cell},
nb = SelectedNotebook[];
FrontEndExecute @ FrontEndToken[nb, "MovePreviousLine"];
SelectionMove[nb, All, Cell];
cell = SelectedCells[nb][[1]];
If[SelectionMove[nb, All, CellGroup] === $Failed,
SelectionMove[cell, All, Cell];
];
If[Last @ SelectedCells[nb] =!= cell,
SelectionMove[cell, All, Cell];
];
];
Ok, these are the last two. These are intended to override Ctrl + Shift + Left and Ctrl + Shift + Right. The big difference is they jump over matching brackets/braces/etc. They are kinda messy and not instant like the builtin bindings feel, but they are fast enough to be usable to me so I will include them here. Add this to KeyEventTranslations.tr
Item[
KeyEvent["Left", Modifiers -> {Control, Shift}],
KernelExecute @ KeyBindings`SelectLeftPrevious[],
MenuEvaluator -> Automatic
],
Item[
KeyEvent["Right", Modifiers -> {Control, Shift}],
KernelExecute @ KeyBindings`SelectRightNext[],
MenuEvaluator -> Automatic
]
And this to init.m
KeyBindings`SelectLeftPrevious[] := (
Quiet @ Module[{nb, range, prevChar, range2},
nb = SelectedNotebook[];
range = "CharacterRange" /.
FrontEndExecute @ FrontEnd`UndocumentedGetSelectionPacket[nb];
If[
range[[1]] =!= range[[2]],
FrontEndExecute @ FrontEndToken[nb, "MovePrevious"]
];
FrontEndExecute @ FrontEndToken[nb, "SelectPrevious"];
prevChar = NotebookRead[nb];
(*Print@prevChar;*)
Which[
(* Is it a backtick or period *)
prevChar === "`" || prevChar === ".",
(* Then do this *)
(*Print@"backtick or period";*)
FrontEndExecute @ FrontEndToken[nb, "SelectPreviousWord"],
(* Is it a commma *)
prevChar === ",",
(* Then do this *)
(*Print@"comma";*)
KeyBindings`SelectLeftWord[],
(* Is it a word *)
MatchQ[prevChar,
Except["]" | "}" | ")" | "\[RightAssociation]" | ">" | "\"" |
"\[RightDoubleBracket]", _String]],
(* If yes then do this *)
(*Print@"word";*)
FrontEndExecute @ FrontEndToken[nb, "MoveNext"];
FrontEndExecute @ FrontEndToken[nb, "ExpandSelection"];
If[
NotebookRead[nb] == ".",
FrontEndExecute @ FrontEndToken[nb, "MovePrevious"];
FrontEndExecute @ FrontEndToken[nb, "SelectPreviousWord"]
];
If[
NotebookRead[nb] == " ",
FrontEndExecute @ FrontEndToken[nb, "SelectPreviousWord"]
],
(* Is it a ] or \[RightDoubleBracket] *)
prevChar === "]" || prevChar === "\[RightDoubleBracket]",
(* If yes then do this *)
(*Print@"] or \[RightDoubleBracket]";*)
FrontEndExecute @ FrontEndToken[nb, "ExpandSelection"];
FrontEndExecute @ FrontEndToken[nb, "MovePrevious"];
FrontEndExecute @ FrontEndToken[nb, "SelectNext"];
If[NotebookRead[nb] === "[",
FrontEndExecute @ FrontEndToken[nb, "ExpandSelection"],
FrontEndExecute @ FrontEndToken[nb, "MoveNext"];
FrontEndExecute @ FrontEndToken[nb, "MoveNextWord"]
],
(* Is it a box thing *)
MatchQ[prevChar, Except[_String]],
(* If yes then do this *)
(*Print@"box thing";*)
Null (* So MMA doesn't get mad on startup *),
(* Then it must be one of } ) \[RightAssociation] > "*)
True,
(*Print@"one of } ) \[RightAssociation] > \"";*)
FrontEndExecute @ FrontEndToken[nb, "ExpandSelection"];
If[
MatchQ[NotebookRead[nb], "*)" | "|>"],
FrontEndExecute @ FrontEndToken[nb, "ExpandSelection"]
]
];
range2 =
"CharacterRange" /.
FrontEndExecute @ FrontEnd`UndocumentedGetSelectionPacket[nb];
SelectionMove[nb, All, Character, range[[2]] - range2 [[1]]]
]
);
KeyBindings`SelectRightNext[] := (
Quiet @ Module[{nb, range, nextChar, range2},
nb = SelectedNotebook[];
range = "CharacterRange" /.
FrontEndExecute @ FrontEnd`UndocumentedGetSelectionPacket[nb];
If[
range[[1]] =!= range[[2]],
FrontEndExecute @ FrontEndToken[nb, "MoveNext"]
];
FrontEndExecute @ FrontEndToken[nb, "SelectNext"];
nextChar = NotebookRead[nb];
(*Print@nextChar;*)
Which[
(* Is it a backtick or period *)
nextChar === "`" || nextChar === ".",
(* Then do this *)
(*Print@"backtick or period";*)
FrontEndExecute @ FrontEndToken[nb, "SelectNextWord"],
(* Is it a commma a space or an indenting newline *)
nextChar === "," || nextChar === " " || nextChar === "\[IndentingNewLine]",
(* Then do this *)
(*Print@"comma space or indenting newline";*)
KeyBindings`SelectRightWord[],
(* Is it a word *)
MatchQ[nextChar,
Except["[" | "{" | "(" | "\[LeftAssociation]" | "<" | "\"" |
"\[LeftDoubleBracket]", _String]],
(* If yes then do this *)
(*Print@"word";*)
FrontEndExecute @ FrontEndToken[nb, "MoveNext"];
FrontEndExecute @ FrontEndToken[nb, "ExpandSelection"],
(* Is it a [ or \[LeftDoubleBracket] *)
nextChar === "[" || nextChar === "\[LeftDoubleBracket]",
(* If yes then do this *)
(*Print@"[ or \[LeftDoubleBracket]";*)
FrontEndExecute @ FrontEndToken[nb, "ExpandSelection"];
FrontEndExecute @ FrontEndToken[nb, "MoveNext"];
KeyBindings`SelectLeftWord[],
(* Is it a box thing *)
MatchQ[nextChar, Except[_String]],
(* If yes then do this *)
(*Print@"box thing";*)
Null (* So MMA doesn't get mad on startup *),
(* Then it must be one of { ( \[LeftAssociation] < " *)
True,
(*Print@"one of { ( \[LeftAssociation] < \"";*)
FrontEndExecute @ FrontEndToken[nb, "ExpandSelection"];
If[
MatchQ[NotebookRead[nb], "(*" | "<|"],
FrontEndExecute @ FrontEndToken[nb, "ExpandSelection"]
]
];
range2 = "CharacterRange" /.
FrontEndExecute @ FrontEnd`UndocumentedGetSelectionPacket[nb];
SelectionMove[nb, Before, CellContents, AutoScroll -> False];
SelectionMove[nb, Next, Character, range[[1]], AutoScroll -> False];
SelectionMove[nb, All, Character, range2[[2]] - range [[1]], AutoScroll -> False]
]
);
Another difference in behavior is that these will only ever increase the size of the selection, e.g. if you just expanded to the left using Ctrl + Shift + Left, Ctrl + Shift + Right will not undo this expansion, instead it will expand the right side of the selection outward (to the right). This isn't ideal behavior, to fix it one would need a global variable, say $ActiveSideOfSelection
, that tracks which side of the selection is the "active" side. I haven't done this (though it wouldn't be that hard to set up).
Ok, thats it for my keybindings, now I'll quickly list the basic functions to write more of these.
SelectionMove
- Is really more for moving between objects in a notebook like cells and graphics, and not so much for text, for which the only useful ways to use it are (let nb
be a NotebookObject
)
SelectionMove[nb, Previous, Character, n]
- move the cursor back n
characters
SelectionMove[nb, Next, Character, n]
- move the cursor forward n
characters
SelectionMove[nb, All, Character, n]
- starting from the left side of the selection, highlight the next n
characters
SelectionMove[nb, Before, CellContents]
- move to the beginning of the cell
FrontEnd`UndocumentedGetSelectionPacket
- this is immensely useful, call using FrontEndExecute
, it inputs a NotebookObject
and outputs (among other things) the beginning and ending locations (in characters from the start of the cell) of the selection. It also outputs the 'type' of the selection as "CellSelectionType"
the possible values I have found for it are
"NoNotebookSelection"
- there is nothing in the notebook
"AboveCell"
- the cursor is between cells and guaranteed to have a cell above it
"BelowCell"
- the cursor is between cells and guaranteed to have a cell below it (not sure why this and "AboveCell"
arn't just called "BetweenCells"
or something like that)
"OnCell"
- the selection is on a cell bracket
"CellRangeSelection"
- the selection is on a cell group
"ContentSelection"
- the cursor is in a cell and there may be something highlighted
FrontEndToken
- call using FrontEndExecute
, it inputs a NotebookObject
and a string called a FrontEndToken, aside from the obvious ones, the ones I find useful are
"CreateInlineCell"
- Like hitting Ctrl + (
"ExpandSelection"
- Like hitting Ctrl + .
Some relevant documentation here and here.
Here is a Github gist with my files. They have other changes from the ones I discussed here, for example, I switched the duties of Ctrl + Enter and Ctrl + . (this way Ctrl + , and Ctrl + . make new rows and columns which makes more sense to me). Most of the changes are marked by comments to make them more visually obvious, also my notes at the top of the files provide some info. init.m
also has some auxiliary functions that I made but may not have ended up using. I have also added a menu called Macros
in MenuSetup.tr
that provide some editing convenience.
6224 is another collection of keybindings, they mostly have to do with cells, so they match well with mine which are mostly text manipulation.