21

[Feature added in version 13.1]

Is there some way I can change Mathematica keyboard behavior so I could select a block of text/code, press tab, and the selection would be indented to right? Similarly, I would like shift + tab to produce indenting to the left.

If it were possible to get this behavior just for multi-line string text, or just in code cells, that would be great, too.

Murta
  • 26,275
  • 6
  • 76
  • 166
  • 3
    Normal mma cells don't have a clear notion of whitespace.. Tab works in code cells, but not for selected blocks, just as the first char of the line. If you're asking such questions, it's usually time to switch to WorkBench. Once I did my life became much easier:) For code cells this would still be very useful though (+1). – Ajasja Jan 06 '14 at 05:40
  • Hi @Ajasja. All my current important code is inside code cells or in packages. I tried workbench, but did not get used to it. I love the new autocomplete interface, collapse and expand code parts and the code execution in place for test, and it's not nice in Workbench. But maybe I have to try Workbench again. – Murta Jan 06 '14 at 10:34
  • Thanks for the accept. So, is my solution really usable for you? It is certainly not refined, and needs some more work to be robust and general. – Leonid Shifrin Feb 22 '14 at 16:19

4 Answers4

10

The following solution seems to work reasonably well, at least on a few examples I have tested. It will be in the spirit of the one I gave to a rather similar earlier question you asked. I wasn't able to make the Tab key work, instead I bound the indenting to the CTRL+ ` combination - but in practice this is almost as easy as pressing Tab key. Also, the following is only a solution for indenting to the right. Indenting to the left is likely also possible, but might be a bit trickier to implement.

Here is the code. This is a generic function to construct a self-overwriting cell:

ClearAll[generateAutoOverwriteCell];
generateAutoOverwriteCell[
   boxes : Except[_?OptionQ] : "", 
   type_String: "Input", 
   opts___?OptionQ
] :=
Module[{},
   SelectionMove[EvaluationNotebook[], All, Cell, AutoScroll -> False];
   SelectionMove[EvaluationNotebook[], Previous, Cell, AutoScroll -> False]; 
   NotebookWrite[EvaluationNotebook[], Cell[BoxData[boxes], type, opts], All]; 
   SelectionMove[EvaluationNotebook[], All, CellContents, AutoScroll -> False]; 
   SelectionMove[EvaluationNotebook[], Previous, Character];
]

This is a relatively simple "formatter" for the selected part of your code:

ClearAll[process, $newlinePattern, $inner];

$newlinePattern = ("\n"|"\[IndentingNewLine]");

process[r_RowBox/;FreeQ[r,$newlinePattern]]:=RowBox[{"\t",r}];

process[x_]:=process[x,False];

process[RowBox[conts_List],flag_]:= RowBox[process[conts,flag]];

process[{left__,sep:$newlinePattern, right___}, flag_]:=
  {
     RowBox[{If[!TrueQ[$inner],"\t",Sequence@@{}],Sequence@@#}] & [
      Block[{$inner = True},process[{left},False]]
     ]
     ,
     sep
     ,
     Sequence @@ Block[{$inner = False},process[{right}, True]]
  };

process[{args__}/;FreeQ[{args},$newlinePattern],True]:={RowBox[{"\t",args}]};

process[x_,_]:=x/.block:{__,$newlinePattern, ___}:>process[block,False];

This is the key action rule, which I bound to the CTRL+ ` combination:

ClearAll[selectionTabRule]
selectionTabRule = 
   {"KeyDown", "`"} :>
      With[{nb = InputNotebook[]}, 
        If[MemberQ[CurrentValue["ModifierKeys"], "Control"],
           With[{sel = NotebookRead[nb]},
              NotebookWrite[nb, process[sel]]
           ],
           (* else *)
           NotebookWrite[nb, "`"]
        ]
      ];

Finally, here is the short-cut to construct such cells:

ClearAll[incell];
incell := generateAutoOverwriteCell[CellEventActions -> {selectionTabRule}];

Now, if you type incell into a new cell and evaluate, you will get a new cell with desired behavior. If you then copy and paste some code into this cell, you can start playing with it. Select the piece of code you'd like to move to the right, and press CTRL+ `. The code you select should be a complete expression or a sequence of expressions.

I am sure there are bugs and limitations in this simple solution. It is more in the spirit of showing how things might work, than being a complete solution here. I just find it interesting to explore the possibilities the FrontEnd can give us.

Leonid Shifrin
  • 114,335
  • 15
  • 329
  • 420
8

Later is better then never. 8 years after my original question, now in version 13.1 we have tab and shift + tab indentation in a native way, with no gambiarra needed. Cool!

Murta
  • 26,275
  • 6
  • 76
  • 166
  • "13.2.0 for Linux x86 (64-bit) (December 7, 2022)" this is not working for me. Can any Linux user confirm this indeed works for them? (It might be a me problem. I use modified hotkeys that I moved from earlier versions.) – Kvothe Feb 23 '23 at 16:55
  • I have "Item[KeyEvent["Tab", Modifiers -> {Shift}], "MovePreviousPlaceHolder"]" in what I believe to be my unmoddified 13.2 KeyEventTranslations.tr file. That does not sound like it is indenting.

    Do you have KeyEvent for indenting? If so could you share it, perhaps it will work if I simply add it.

    – Kvothe Feb 23 '23 at 17:00
  • Oops I probably misunderstood what you meant. While selecting multiple lines it seems that Tab natively indents multiple lines. (I am not sure what Shift +Tab is supposed to do but it doesn't do anything Indent related for me probably due to the KeyEvent mentioned in my comment above.) – Kvothe Feb 23 '23 at 17:20
6

I do much the same as Leonid, except I generally first convert the \[IndentingNewLine] structure into the appropriate tabified block:

indentingNewLineReplace[r : RowBox[data_]] :=
  RowBox@
   Replace[data, {
     "{" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["{"]++,
       "{"
       ],
     "}" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["{"] =
        Max@{$indentationUnbalancedBrackets["{"] - 1, 0},
       "}"
       ],
     "[" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["["]++,
       "["
       ],
     "]" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["["] =
        Max@{$indentationUnbalancedBrackets["["] - 1, 0},
       "]"
       ],
     "(" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["("]++,
       "("
       ],
     ")" :>
      CompoundExpression[
       $indentationUnbalancedBrackets["("] =
        Max@{$indentationUnbalancedBrackets["("] - 1, 0},
       ")"
       ],
     r2_RowBox :>
      indentingNewLineReplace[r2],
     $indentingNewLine :>
      CompoundExpression[
       Map[
        Which[
          $indentationUnbalancedBrackets[#] > \
$intentationPreviousLevels[#],
          $indentationLevel[#]++,
          $indentationUnbalancedBrackets[#] < \
$intentationPreviousLevels[#],
          $indentationLevel[#] =
           Max@{$indentationLevel[#] - 1, 0}
          ] &,
        Keys@$indentationLevel],
       $intentationPreviousLevels = $indentationUnbalancedBrackets,
       "\n" <>
        If[Total@$indentationLevel > 0,
         StringRepeat["\t", Total@$indentationLevel],
         ""
         ]
       ]
     },
    1];

IndentingNewLineReplace[r : RowBox[data_]] :=
  Block[{
    $indentationUnbalancedBrackets =
     <|"[" -> 0, "{" -> 0, "(" -> 0|>,
    $intentationPreviousLevels =
     <|"[" -> 0, "{" -> 0, "(" -> 0|>,
    $indentationLevel =
     <|"[" -> 0, "{" -> 0, "(" -> 0|>
    },
   indentingNewLineReplace[r]
   ];
IndentingNewLineReplace[s_String] :=
  s;

IndentationReplace[nb_: Automatic] :=

  With[{inputNotebook = Replace[nb, Automatic :> InputNotebook[]]},
   With[{selection = IndentationSelection@inputNotebook},
    With[{write = IndentingNewLineReplace@selection},
     NotebookWrite[inputNotebook, write,
      If[MatchQ[write, _String?(StringMatchQ[Whitespace])],
       After,
       All]]
     ]
    ]
   ];

Then I do a standard simple recursive replacement:

IndentationSelection[inputNotebook_] :=

  Replace[NotebookRead@inputNotebook, {
    Cell[BoxData[d_] | d_String, ___] :>
     CompoundExpression[
      SelectionMove[First@SelectedCells[], All, CellContents],
      d]
    }];

indentationAddTabsRecursiveCall[RowBox[d : {___}]] :=
  RowBox@
   Replace[d, {
     r_RowBox :>
      indentationAddTabsRecursiveCall[r],
     s_String?(StringMatchQ[$indentingNewLine ~~ ___]) :>

      StringInsert[StringDrop[s, 1], "\n\t", 1],
     s_String?(StringMatchQ["\n" ~~ ___]) :>

      StringInsert[s, "\t", 2]
     },
    1];
indentationAddTabs[sel_] :=
  Replace[
   sel, {
    {} :> "\t",
    _String :>
     StringReplace[sel, {
       "\n" :> "\n\t",
       StartOfString :> "\t"
       }],
    _ :>
     Replace[indentationAddTabsRecursiveCall[sel],
      RowBox[{data___}] :>
       RowBox[{"\t", data}]
      ]
    }];

IndentationIncrease[nb_: Automatic] :=

  With[{inputNotebook = Replace[nb, Automatic :> InputNotebook[]]},
   With[{write = 
      indentationAddTabs@IndentationSelection@inputNotebook},
    NotebookWrite[inputNotebook, write,
     If[MatchQ[write, _String?(StringMatchQ[Whitespace])],
      After,
      All]]
    ]
   ];

And then you can put this def in a stylesheet or attach it to a cell / notebook:

IndentationEvent[] :=
  If[AllTrue[
    {"OptionKey", "ShiftKey"},
    CurrentValue[EvaluationNotebook[], #] &
    ],
   IndentationRestore[],
   Which[
    Not@FreeQ[NotebookRead@EvaluationNotebook[], $indentingNewLine],
    IndentationReplace[],
    CurrentValue["OptionKey"],
    IndentationDecrease[],
    True,
    IndentationIncrease[]
    ]
   ];

{
 {"KeyDown", "\t"} :>
  Quiet@Check[
    Needs["BTools`"];
    IndentationEvent[],
    SetAttributes[EvaluationCell[], CellEventActions -> None]
    ],
 PassEventsDown -> False
 }

It's a lot of code, but it is generally quite robust in my experience. The only standard issue I have is that the indenting-newline to tab replacer doesn't appropriately pick up special indenting characters like = and ->. It only catches the block indents.

b3m2a1
  • 46,870
  • 3
  • 92
  • 239
5

Here is a much more modest try for blocks of code. :) You could expand it to cover text like @LeonidShifrin has shown. It could also be reversed to unindent.

SetOptions[$FrontEnd, 
 FrontEndEventActions -> {
  {"KeyDown", "\t"} :> 
   NotebookWrite[
    InputNotebook[], 
    Insert[
     NotebookRead[InputNotebook[]] /. 
      "\[IndentingNewLine]" -> 
       Sequence["\[IndentingNewLine]", "\t"],
     "\t", {1, 1}]]}]

Select the whole lines (at least make sure you have the beginning of each line, including the first) and press tab:

a
b
c

... a

... b

... c

mfvonh
  • 8,460
  • 27
  • 42
  • This is nice. Is it possible to retain basic tabbing (inserting a tab character) when there's no selection? – Rico Picone May 27 '14 at 04:17
  • @RicoPicone after :> check to see if CurrentValue["SelectionData"] === $Failed (= nothing is selected), then decide what to do based on that. (Too cryptic?) – mfvonh May 27 '14 at 04:19
  • Thanks, I was able to get that working. However, I noticed that under many circumstances, the block-tabbing fails. Even when selecting entire lines, often only the first line is indented. Sometimes the other lines are also indented, but one line from the selection is not (like the second line). I was unable to detect rules for these occurrences, and I suspect some extra hidden characters are running around that I can't see (is there any way to show them?). – Rico Picone May 27 '14 at 15:28
  • @RicoPicone Yeah, that code won't work unless you have manually inserted line breaks (it's just adding a tab at the beginning of the selection and after every line break). Maybe what you want is changing LineIndent. Press Ctrl+Shift+O, then type LineIndent in the "Lookup" box. You can change it for individual cells or the whole notebook that way. Or you can do it via the StyleSheet. – mfvonh May 27 '14 at 16:51
  • @RicoPicone Also, I am just repeating others here, but you may consider Wolfram Workbench or (in my opinion much better) halirutan's MMA plugin for IDEA. I find it much easier to code in an IDE and use notebooks for testing/etc. – mfvonh May 27 '14 at 16:55
  • Can the MMA plugin be used for notebooks? I write packages and scripts, and I have MMA syntax highlighting in Sublime Text 2 for that, which is a pretty good editor for my purposes. But when I'm developing interactively and generating graphics, which is most of the time, I work in notebooks. I find the native editing of notebooks to be clunky, with its auto-everything and lack of features like block-indent. Do you just work without notebooks most of the time? – Rico Picone May 28 '14 at 18:20
  • @RicoPicone You can have your cake and eat it too. I almost always have both open, and they can work together. The IDE launches a kernel in debug mode and attaches a front end to it, so all your definitions (from the IDE) are available (and you can interactively debug!). You can even make changes to the code in the IDE and save and they will update in the interactive session. – mfvonh May 28 '14 at 22:30
  • Cool. I'll have to check it out! – Rico Picone May 29 '14 at 18:10
  • I installed it and it seems like a nice editor for packages and scripts, but I don't see anywhere how to use the same kernel in the IDE and a MMA notebook. Are there instructions anywhere for this? – Rico Picone May 29 '14 at 22:15