3
$\begingroup$

How would one add (four custom keyboard-based not palette-based) shortcuts for moving the cellgroup containing the cursor up and down and in and out (akin to tab indent) relative to adjacent cells.

Here's an example of this idea working in emacs:

enter image description here

The key thing is that the action takes the content nested below it along for the ride, i.e. reordering branches of the notebook tree.

Notes:

  • It would be great if it works cross-platform, but I'll settle for changes in the .tr on MacOS.
  • You can assume an outline or custom notebook stylesheet
  • It must work in the FE kernel to be performant
  • Related but unanswered: How to move a cell up or down with a keystroke?
$\endgroup$
12
  • 1
    $\begingroup$ This is no longer something I work on much, but unfortunately this is unlikely to be possible purely in the FrontEnd`Private or FE` contexts, given that the front end itself doesn't have a mechanism for rearranging the children of a FrontEndObject (unless things have changed in the past 4 years). This means you'll want to a) freeze notebook updates (easy) b) copy the current cell above the previous one (shouldn't be terrible with FrontEnd packets) c) delete the current cell assuming the previous worked and d) no matter what unfreeze the notebook updates. Not easy but not terrible. $\endgroup$
    – b3m2a1
    Commented Jan 9 at 7:13
  • 1
    $\begingroup$ How do you freeze notebook updates? $\endgroup$
    – user5601
    Commented Jan 9 at 14:49
  • 1
    $\begingroup$ See the FE packet list for details but you use the packets "NotebookSuspendScreenUpdates", and "NotebookResumeScreenUpdates" $\endgroup$
    – b3m2a1
    Commented Jan 9 at 16:47
  • $\begingroup$ @b3m2a1 That sounds right to me, but I'm not sure how they would ensure no data loss during the operation copy delete step. $\endgroup$
    – M.R.
    Commented Jan 12 at 0:28
  • $\begingroup$ @M.R. you can copy and check you got a valid FrontEndObject out from the FE packet (they return) before deleting, but again I think there's no way to ensure no kernel involvement $\endgroup$
    – b3m2a1
    Commented Jan 12 at 1:37

1 Answer 1

3
$\begingroup$

I dug up my old custom key events answer to get this to work, but here's the real deal, a key event to move your cell up by one which also has protection against being at the top of the notebook.

First to allow dynamic key event binding

Get["https://raw.githubusercontent.com/b3m2a1/mathematica-tools/master/\
CustomKeyEvents.wl"];

Then

AddKeyEvent[
  "u",
  KernelExecute[
   Block[
    {
     curCell,
     prev,
     contents
     },
    {curCell, contents, prev} =
     FrontEndExecute[{
        FrontEnd`NotebookSuspendScreenUpdates[FrontEnd`SelectedNotebook[]],
        FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], All, Cell],
        FrontEnd`SelectedObject[FrontEnd`SelectedNotebook[]],
        FrontEnd`NotebookRead[FrontEnd`SelectedNotebook[]],
        FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], Previous, Cell],
        FrontEnd`SelectedObject[FrontEnd`SelectedNotebook[]],
        FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], Before, Cell]
        }
       ][[{3, 4, 6}]];
    If[MatchQ[curCell, _CellObject] && MatchQ[prev, _CellObject],
     FrontEndExecute@{
       FrontEnd`NotebookWrite[
        FrontEnd`SelectedNotebook[],
        contents,
        None
        ],
       FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], Previous, Cell],
       FrontEnd`NotebookDelete[curCell],
       FrontEnd`NotebookResumeScreenUpdates[FrontEnd`SelectedNotebook[]]
       },
     FrontEndExecute@
      FrontEnd`NotebookResumeScreenUpdates[FrontEnd`SelectedNotebook[]]
     ]
    ]
   ],
  "Modifiers" -> {"Command"}
  ];

Just type Command+u and it will move the cell, even reselecting the cell afterwards to make it easy to keep copying up.

Couldn't help myself, so here's going down, too

AddKeyEvent["d",
  KernelExecute[
   Block[
    {curCell, next, contents},
    {curCell, contents, next} =
     FrontEndExecute[{
        FrontEnd`NotebookSuspendScreenUpdates[FrontEnd`SelectedNotebook[]], 
        FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], All, Cell], 
        FrontEnd`SelectedObject[FrontEnd`SelectedNotebook[]], 
        FrontEnd`NotebookRead[FrontEnd`SelectedNotebook[]], 
        FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], Next, Cell], 
        FrontEnd`SelectedObject[FrontEnd`SelectedNotebook[]], 
        FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], After, Cell]
        }][[{3, 4, 6}]];
    If[MatchQ[curCell, _CellObject] && MatchQ[next, _CellObject],
     FrontEndExecute@{
       FrontEnd`NotebookWrite[FrontEnd`SelectedNotebook[], contents, None], 
       FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], Previous, Cell], 
       FrontEnd`NotebookDelete[curCell], 
       FrontEnd`NotebookResumeScreenUpdates[FrontEnd`SelectedNotebook[]]
       },
     FrontEndExecute@
      FrontEnd`NotebookResumeScreenUpdates[FrontEnd`SelectedNotebook[]]]
    ]
   ],
  "Modifiers" -> {"Command"}
  ];

Button

M.R. wanted an example, so digging deep into the memory banks here's what I have. I can't promise it's optimal but it's only 2 calls to the front end and the process goes by without the user ever being able to see anything happen

Button[
 "Click Me to Move the Current Cell",
 Block[
  {
   curCell,
   prev,
   contents
   },
  {curCell, contents} =
   FrontEndExecute[{
      FrontEnd`NotebookSuspendScreenUpdates[FrontEnd`SelectedNotebook[]],
      FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], All, Cell],
      FrontEnd`SelectedObject[FrontEnd`SelectedNotebook[]],
      FrontEnd`NotebookRead[FrontEnd`SelectedNotebook[]],
      FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], Previous, Cell],
      FrontEnd`SelectionMove[FrontEnd`SelectedNotebook[], Before, Cell]
      }
     ][[{3, 4}]];
  FrontEndExecute@{
    FrontEnd`NotebookWrite[
     FrontEnd`SelectedNotebook[],
     contents,
     Cell
     ],
    FrontEnd`NotebookDelete[curCell],
    FrontEnd`NotebookResumeScreenUpdates[FrontEnd`SelectedNotebook[]]
    }
  ]
 ]

I don't think there's a faster way to get the selection than selection move, annoyingly. If I could use FrontEnd`PreviousSiblingObject inside the call or chain some of this stuff better I think I could do this in 1 call.

I also feel like I have a vague memory of copying a cell without needing to read out its contents first? But my memory could just be faulty.

It's up to you to figure out how to work this into the .tr file, or you can work with the Menu system (I know I had some library for doing dynamic key rebinding by sticking stuff in menus a number of years back)

As for the indent / dedent problem, I had some solution for this I'd been using since 2017 which worked well for me, but who knows if the script I stuck on GitHub can still be loaded without issues (see this)

$\endgroup$
4
  • $\begingroup$ This works great for single cells, but what about moving the current cell AND the cells grouped under it (even when they are collapsed)... Is that a simple extension? $\endgroup$
    – user5601
    Commented Feb 1 at 19:43
  • $\begingroup$ I tried replacing Cell with CellGroup in your code but that breaks things... $\endgroup$
    – user5601
    Commented Feb 1 at 20:00
  • $\begingroup$ This last step requires being able to select the cell or the cellgroup ONLY IF it is the root/top cell of the group below it (not the cellgroup above). Not sure if there's a way to do that? $\endgroup$
    – user5601
    Commented Feb 1 at 20:20
  • $\begingroup$ @user5601 not without multiple calls back to the kernel $\endgroup$
    – b3m2a1
    Commented Feb 2 at 0:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.